Browse Source

ly 裂解炉温度预测

ly 2 months ago
parent
commit
e0de5afe9b

+ 95 - 0
master/src/main/java/com/ruoyi/common/utils/MathUtil.java

@@ -0,0 +1,95 @@
+package com.ruoyi.common.utils;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * 数学工具类
+ * 提供线性回归等数学计算方法
+ */
+public class MathUtil {
+
+    /**
+     * 通过线性回归预测到达限值的日期
+     *
+     * @param dateValueMap 日期字符串到数值的映射,格式为"yyyy-MM-dd"
+     * @param limit 限值
+     * @return Map包含预测日期("date")和斜率方程("equation"),如果无法预测则返回null
+     */
+    public static Map<String, String> predictDateToReachLimit(Map<String, Double> dateValueMap, double limit) {
+        if (dateValueMap == null || dateValueMap.size() < 2) {
+            return null;
+        }
+
+        // 按日期排序
+        List<Map.Entry<String, Double>> sortedEntries = new ArrayList<>(dateValueMap.entrySet());
+        sortedEntries.sort(Map.Entry.comparingByKey());
+
+        // 转换为数值进行计算(使用相对于第一个日期的天数,但从1开始)
+        List<Double> xValues = new ArrayList<>();
+        List<Double> yValues = new ArrayList<>();
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+        LocalDate firstDate = LocalDate.parse(sortedEntries.get(0).getKey(), formatter);
+
+        for (Map.Entry<String, Double> entry : sortedEntries) {
+            LocalDate date = LocalDate.parse(entry.getKey(), formatter);
+            long daysDiff = date.toEpochDay() - firstDate.toEpochDay();
+            xValues.add((double) (daysDiff + 1)); // 从1开始,而不是从0开始
+            yValues.add(entry.getValue());
+        }
+
+        // 计算线性回归参数
+        double[] regressionParams = calculateLinearRegression(xValues, yValues);
+        double slope = regressionParams[0];
+        double intercept = regressionParams[1];
+
+        // 如果斜率为0或接近0,无法预测
+        if (Math.abs(slope) < 1e-10) {
+            return null;
+        }
+
+        // 计算到达限值需要的天数(相对于第一个日期)
+        double daysToLimit = (limit - intercept) / slope;
+
+        // 基于第一个日期计算预测日期
+        LocalDate predictedDate = firstDate.plusDays((long) Math.round(daysToLimit));
+
+        // 构建返回结果:预测日期 + 斜率方程
+        String equation = String.format("y = %.4fx + %.4f", slope, intercept);
+        Map<String, String> result = new HashMap<>();
+        result.put("date", predictedDate.toString());
+        result.put("equation", equation);
+        return result;
+    }
+
+    /**
+     * 计算线性回归参数
+     *
+     * @param xValues x值列表
+     * @param yValues y值列表
+     * @return [斜率, 截距]
+     */
+    private static double[] calculateLinearRegression(List<Double> xValues, List<Double> yValues) {
+        int n = xValues.size();
+        double sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
+
+        // 计算各项和
+        for (int i = 0; i < n; i++) {
+            double x = xValues.get(i);
+            double y = yValues.get(i);
+
+            sumX += x;
+            sumY += y;
+            sumXY += x * y;
+            sumXX += x * x;
+        }
+
+        // 计算斜率和截距
+        double slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
+        double intercept = (sumY - slope * sumX) / n;
+
+        return new double[]{slope, intercept};
+    }
+}

+ 39 - 0
master/src/main/java/com/ruoyi/common/utils/MathUtilExample.java

@@ -0,0 +1,39 @@
+package com.ruoyi.common.utils;
+
+import java.util.*;
+
+/**
+ * MathUtil使用示例
+ */
+public class MathUtilExample {
+
+    public static void main(String[] args) {
+        // 创建测试数据:日期和对应的数值(与图片中的数据点对应)
+        Map<String, Double> data = new LinkedHashMap<>();
+        data.put("2024-01-01", 100.0);
+        data.put("2024-01-02", 121.0);
+        data.put("2024-01-03", 145.0);
+        data.put("2024-01-04", 154.0);
+        data.put("2024-01-05", 190.0);
+        data.put("2024-01-06", 200.0);
+        data.put("2024-01-07", 229.0);
+        data.put("2024-01-08", 238.0);
+        data.put("2024-01-09", 266.0);
+        // 预测到达限值200的日期
+        double limit = 300.0;
+        Map<String, String> result = MathUtil.predictDateToReachLimit(data, limit);
+
+        System.out.println("数据点:");
+        data.forEach((date, value) -> System.out.println(date + " -> " + value));
+        System.out.println();
+        System.out.println("预测到达限值 " + limit + " 的结果:");
+        System.out.println("预测日期:" + result.get("date"));
+        System.out.println("斜率方程:" + result.get("equation"));
+
+        // 测试其他限值
+        Map<String, String> result2 = MathUtil.predictDateToReachLimit(data, 350.0);
+        System.out.println("\n预测到达限值 350 的结果:");
+        System.out.println("预测日期:" + result2.get("date"));
+        System.out.println("斜率方程:" + result2.get("equation"));
+    }
+}

+ 358 - 21
master/src/main/java/com/ruoyi/project/production/controller/TFurnanceTemperatureController.java

@@ -2,10 +2,9 @@ package com.ruoyi.project.production.controller;
 
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.List;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
 
 import com.github.stuxuhai.jpinyin.PinyinException;
 import com.ruoyi.common.jpush.JiGuangPushService;
@@ -17,7 +16,7 @@ import com.ruoyi.project.plant.domain.TStaffmgr;
 import com.ruoyi.project.plant.service.ITStaffmgrService;
 import com.ruoyi.project.production.controller.vo.FurnancePressureFvpVO;
 import com.ruoyi.project.production.controller.vo.FurnanceSummaryVO;
-import com.ruoyi.project.production.controller.vo.FurnanceTemperatureCoilVO;
+import com.ruoyi.project.production.controller.vo.FurnanceTemperatureVO;
 import com.ruoyi.project.production.controller.vo.FurnanceTemperatureVO;
 import com.ruoyi.project.production.domain.TFurnancePressure;
 import com.ruoyi.project.production.domain.TFurnanceTemperature;
@@ -28,6 +27,10 @@ import com.ruoyi.project.system.service.ISysConfigService;
 import com.ruoyi.project.system.service.ISysDictTypeService;
 import com.ruoyi.project.system.service.ISysUserService;
 import org.springframework.security.access.prepost.PreAuthorize;
+import com.ruoyi.common.utils.MathUtil;
+import com.ruoyi.project.production.controller.vo.FurnanceTemperatureCoilVO;
+import java.text.SimpleDateFormat;
+
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -990,43 +993,43 @@ public class TFurnanceTemperatureController extends BaseController
             tFurnanceTemperature.setFurnanceName("H109");
             List<TFurnanceTemperature> list109 = tFurnanceTemperatureService.selectTFurnanceTemperatureList(tFurnanceTemperature);
             logger.debug("查询H109数据条数: {}", list109.size());
-            
+
             tFurnanceTemperature.setFurnanceName("H110");
             List<TFurnanceTemperature> list110 = tFurnanceTemperatureService.selectTFurnanceTemperatureList(tFurnanceTemperature);
             logger.debug("查询H110数据条数: {}", list110.size());
-            
+
             tFurnanceTemperature.setFurnanceName("H111");
             List<TFurnanceTemperature> list111 = tFurnanceTemperatureService.selectTFurnanceTemperatureList(tFurnanceTemperature);
             logger.debug("查询H111数据条数: {}", list111.size());
-            
+
             tFurnanceTemperature.setFurnanceName("H112");
             List<TFurnanceTemperature> list112 = tFurnanceTemperatureService.selectTFurnanceTemperatureList(tFurnanceTemperature);
             logger.debug("查询H112数据条数: {}", list112.size());
-            
+
             tFurnanceTemperature.setFurnanceName("H113");
             List<TFurnanceTemperature> list113 = tFurnanceTemperatureService.selectTFurnanceTemperatureList(tFurnanceTemperature);
             logger.debug("查询H113数据条数: {}", list113.size());
-            
+
             tFurnanceTemperature.setFurnanceName("H114");
             List<TFurnanceTemperature> list114 = tFurnanceTemperatureService.selectTFurnanceTemperatureList(tFurnanceTemperature);
             logger.debug("查询H114数据条数: {}", list114.size());
-            
+
             tFurnanceTemperature.setFurnanceName("H115");
             List<TFurnanceTemperature> list115 = tFurnanceTemperatureService.selectTFurnanceTemperatureList(tFurnanceTemperature);
             logger.debug("查询H115数据条数: {}", list115.size());
-            
+
             tFurnanceTemperature.setFurnanceName("H116");
             List<TFurnanceTemperature> list116 = tFurnanceTemperatureService.selectTFurnanceTemperatureList(tFurnanceTemperature);
             logger.debug("查询H116数据条数: {}", list116.size());
-            
+
             tFurnanceTemperature.setFurnanceName("H117");
             List<TFurnanceTemperature> list117 = tFurnanceTemperatureService.selectTFurnanceTemperatureList(tFurnanceTemperature);
             logger.debug("查询H117数据条数: {}", list117.size());
-            
+
             tFurnanceTemperature.setFurnanceName("H118");
             List<TFurnanceTemperature> list118 = tFurnanceTemperatureService.selectTFurnanceTemperatureList(tFurnanceTemperature);
             logger.debug("查询H118数据条数: {}", list118.size());
-            
+
             tFurnanceTemperature.setFurnanceName("H130");
             List<TFurnanceTemperature> list130 = tFurnanceTemperatureService.selectTFurnanceTemperatureList(tFurnanceTemperature);
             logger.debug("查询H130数据条数: {}", list130.size());
@@ -1301,13 +1304,207 @@ public class TFurnanceTemperatureController extends BaseController
         }
     }
 
+    /**
+     * 预测每个PASS达到阈值(默认1080)的日期(最近60天数据),仅返回一条结果,含各PASS预测日期
+     */
+    @PreAuthorize("@ss.hasPermi('production:temperature:list')")
+    @GetMapping("/coilPredict1080")
+    public AjaxResult coilPredict1080(TFurnanceTemperature tFurnanceTemperature) {
+        try {
+            // 组装最近60天时间范围
+            Date today = new Date();
+            Calendar endCal = Calendar.getInstance();
+            endCal.setTime(today);
+            endCal.add(Calendar.DAY_OF_MONTH, 1);
+            endCal.set(Calendar.HOUR_OF_DAY, 0);
+            endCal.set(Calendar.MINUTE, 0);
+            endCal.set(Calendar.SECOND, 0);
+            endCal.set(Calendar.MILLISECOND, 0);
+
+            Calendar startCal = Calendar.getInstance();
+            startCal.setTime(today);
+            startCal.add(Calendar.DAY_OF_MONTH, -60);
+            startCal.set(Calendar.HOUR_OF_DAY, 0);
+            startCal.set(Calendar.MINUTE, 0);
+            startCal.set(Calendar.SECOND, 0);
+            startCal.set(Calendar.MILLISECOND, 0);
+
+            // 需要处理的炉号
+            String[] furnaces = {"H109","H110","H111","H112","H113","H114","H115","H116","H117","H118","H130"};
+            // 返回结构参考 /coil:一条记录包含所有炉的数组字符串(这里用日期数组字符串代替)
+            FurnanceTemperatureVO vo = new FurnanceTemperatureVO();
+            vo.setRecordTime(new Date());
+            SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");
+
+            for (String furnace : furnaces) {
+                // 先查询该炉在窗口内的所有原始数据,仅查询一次,避免 N+1
+                TFurnanceTemperature q = new TFurnanceTemperature();
+                q.setFurnanceName(furnace);
+                q.setStartDate(startCal.getTime());
+                q.setEndDate(endCal.getTime());
+                List<TFurnanceTemperature> raw = tFurnanceTemperatureService.selectTFurnanceTemperatureList(q);
+                // 按时间升序
+                raw.sort(Comparator.comparing(TFurnanceTemperature::getRecordTime));
+                // 找到最近一次 status 为 1 或 2 的位置
+                int pivot = -1;
+                for (int i = 0; i < raw.size(); i++) {
+                    String st = raw.get(i).getStatus();
+                    if ("1".equals(st) || "2".equals(st)) {
+                        pivot = i;
+                    }
+                }
+                // 仅保留“最近一次状态为1或2”之后的所有数据;忽略值为0在后续组装阶段处理
+                List<TFurnanceTemperature> filtered = new ArrayList<>();
+                int startIdx = (pivot >= 0) ? (pivot + 1) : 0;
+                for (int i = startIdx; i < raw.size(); i++) {
+                    filtered.add(raw.get(i));
+                }
+
+                // 针对每个PASS构建数据并做线性回归预测
+                int passCount = "H130".equals(furnace) ? 12 : 8;
+                Map<String, String> passToDate = new LinkedHashMap<>();
+
+                for (int passNo = 1; passNo <= passCount; passNo++) {
+                    // 如果过滤后仍少于2条,则不进行计算,直接返回"-"
+                    if (filtered.size() < 2) {
+                        logger.info("coilPredict1080 skip | furnace={} pass={} reason=insufficient_points after pivot", furnace, passNo);
+                        passToDate.put("PASS" + passNo, "-");
+                        continue;
+                    }
+
+                    // 组装日期->值,并打印调试
+                    Map<String, Double> series = new LinkedHashMap<>();
+                    List<String> debugPairs = new ArrayList<>();
+                    String passKey = "pass" + passNo;
+                    for (TFurnanceTemperature item : filtered) {
+                        // 使用与 /coil 一致的规则:每条记录中该 PASS 对应的最大值(getMaxValue 用于拼接并取最大)
+                        int value;
+                        if ("H109".equals(furnace)) {
+                            value = setPassMaxValueH109(item, passKey);
+                        } else if ("H130".equals(furnace)) {
+                            value = setPassMaxValueH130(item, passKey);
+                        } else {
+                            value = setPassMaxValueH11x(item, passKey);
+                        }
+                        String d = fmt.format(item.getRecordTime());
+                        // 只纳入有效数值
+                        if (value > 0) {
+                            series.put(d, (double) value);
+                            debugPairs.add(d + "=" + value);
+                        }
+                    }
+                    // 只在调试模式下打印详细数据
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("coilPredict1080 input | furnace={} pass={} points={} data={}", furnace, passNo, series.size(), String.join(" | ", debugPairs));
+                        // 打印回归前的数据点(日期, 值)单行输出,例如 [2025-01-01, 121], [2025-01-02, 145], ...
+                        if (!series.isEmpty()) {
+                            List<String> datePairs = new ArrayList<>();
+                            for (Map.Entry<String, Double> e : series.entrySet()) {
+                                datePairs.add("[" + e.getKey() + ", " + e.getValue().intValue() + "]");
+                            }
+                            logger.debug("coilPredict1080 series (date,val) | furnace={} pass={} => {}", furnace, passNo, String.join(", ", datePairs));
+                        }
+                    }
+
+                    // 使用MathUtil线性回归预测达到1080的日期,并打印结果与斜率
+                    String date = "-";
+                    if (series.size() >= 2) {
+                        Map<String, String> res = MathUtil.predictDateToReachLimit(series, 1080.0);
+                        if (res != null) {
+                            String equation = res.get("equation"); // 形如 y = 20.4833x + 80.1400
+                            String slopeStr = null;
+                            if (equation != null) {
+                                try {
+                                    String right = equation.split("=")[1].trim();
+                                    slopeStr = right.split("x")[0].trim();
+                                } catch (Exception ignore) {}
+                            }
+                            if (res.get("date") != null) {
+                                date = res.get("date");
+                                // 如果预测结果早于或等于最后一天数据日期,则不显示(置为“-”)
+                                try {
+                                    DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+                                    String lastKey = null;
+                                    for (String k : series.keySet()) { lastKey = k; }
+                                    if (lastKey != null) {
+                                        LocalDate predicted = LocalDate.parse(date, df);
+                                        LocalDate last = LocalDate.parse(lastKey, df);
+                                        if (!predicted.isAfter(last)) {
+                                            date = "-";
+                                        }
+                                    }
+                                } catch (Exception ignore) {}
+                            }
+                            // 只在调试模式下打印详细结果
+                            if (logger.isDebugEnabled()) {
+                                logger.debug("coilPredict1080 result | furnace={} pass={} predictedDate={} slope={} equation={}",
+                                        furnace, passNo, date, slopeStr, equation);
+                            }
+                        }
+                    }
+                    passToDate.put("PASS" + passNo, date);
+                }
+
+                // 写入VO对应字段:与 /coil 保持相同字段名与字符串格式(逗号分隔)
+                String joined = String.join(",",
+                        passToDate.getOrDefault("PASS1","-"),
+                        passToDate.getOrDefault("PASS2","-"),
+                        passToDate.getOrDefault("PASS3","-"),
+                        passToDate.getOrDefault("PASS4","-"),
+                        passToDate.getOrDefault("PASS5","-"),
+                        passToDate.getOrDefault("PASS6","-"),
+                        passToDate.getOrDefault("PASS7","-"),
+                        passToDate.getOrDefault("PASS8","-")
+                );
+                if ("H109".equals(furnace)) vo.setH109Out(joined);
+                if ("H110".equals(furnace)) vo.setH110Out(joined);
+                if ("H111".equals(furnace)) vo.setH111Out(joined);
+                if ("H112".equals(furnace)) vo.setH112Out(joined);
+                if ("H113".equals(furnace)) vo.setH113Out(joined);
+                if ("H114".equals(furnace)) vo.setH114Out(joined);
+                if ("H115".equals(furnace)) vo.setH115Out(joined);
+                if ("H116".equals(furnace)) vo.setH116Out(joined);
+                if ("H117".equals(furnace)) vo.setH117Out(joined);
+                if ("H118".equals(furnace)) vo.setH118Out(joined);
+                if ("H130".equals(furnace)) {
+                    String joined130 = String.join(",",
+                        passToDate.getOrDefault("PASS1","-"),
+                        passToDate.getOrDefault("PASS2","-"),
+                        passToDate.getOrDefault("PASS3","-"),
+                        passToDate.getOrDefault("PASS4","-"),
+                        passToDate.getOrDefault("PASS5","-"),
+                        passToDate.getOrDefault("PASS6","-"),
+                        passToDate.getOrDefault("PASS7","-"),
+                        passToDate.getOrDefault("PASS8","-"),
+                        passToDate.getOrDefault("PASS9","-"),
+                        passToDate.getOrDefault("PASS10","-"),
+                        passToDate.getOrDefault("PASS11","-"),
+                        passToDate.getOrDefault("PASS12","-")
+                    );
+                    vo.setH130Out(joined130);
+                }
+            }
+
+            // 只返回一条记录,与 /coil 保持一致
+            List<FurnanceTemperatureVO> one = new ArrayList<>();
+            one.add(vo);
+
+            // 汇总日志:显示接口执行情况
+            logger.info("coilPredict1080 completed | furnaces={} timeWindow={}days", furnaces.length, 60);
+            return AjaxResult.success(one);
+        } catch (Exception e) {
+            logger.error("预测PASS达到1080接口执行异常", e);
+            throw e;
+        }
+    }
+
     /**
      * 查询裂解炉炉管测温COIL趋势分析
      */
     @PreAuthorize("@ss.hasPermi('production:pressure:list')")
     @GetMapping("/coilAnalysis")
     public AjaxResult coilAnalysis(TFurnanceTemperature tFurnanceTemperature) {
-        logger.info("开始执行查询裂解炉炉管测温COIL趋势分析接口,入参: furnanceName={}, pass={}, startDate={}, endDate={}", 
+        logger.info("开始执行查询裂解炉炉管测温COIL趋势分析接口,入参: furnanceName={}, pass={}, startDate={}, endDate={}",
                    tFurnanceTemperature.getFurnanceName(), tFurnanceTemperature.getPassNo(),
                    tFurnanceTemperature.getStartDate(), tFurnanceTemperature.getEndDate());
         try {
@@ -1343,7 +1540,7 @@ public class TFurnanceTemperatureController extends BaseController
             }
             List<TFurnanceTemperature> tFurnanceTemperatures = tFurnanceTemperatureService.selectCoilAnalysis(temperature);//原始数据列表
             logger.info("查询到原始数据条数: {}", tFurnanceTemperatures.size());
-            
+
             List<FurnanceTemperatureCoilVO> coilVoList = new ArrayList<FurnanceTemperatureCoilVO>();//返回数据列表
             //数据组装
             for (TFurnanceTemperature obj : tFurnanceTemperatures) {
@@ -1486,7 +1683,7 @@ public class TFurnanceTemperatureController extends BaseController
     @GetMapping("/list")
     public TableDataInfo list(TFurnanceTemperature tFurnanceTemperature)
     {
-        logger.info("开始执行查询裂解炉炉管测温列表接口,入参: furnanceName={}, recordTime={}", 
+        logger.info("开始执行查询裂解炉炉管测温列表接口,入参: furnanceName={}, recordTime={}",
                    tFurnanceTemperature.getFurnanceName(), tFurnanceTemperature.getRecordTime());
         try {
             startPage();
@@ -1507,7 +1704,7 @@ public class TFurnanceTemperatureController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(TFurnanceTemperature tFurnanceTemperature)
     {
-        logger.info("开始执行导出裂解炉炉管测温列表接口,入参: furnanceName={}, recordTime={}", 
+        logger.info("开始执行导出裂解炉炉管测温列表接口,入参: furnanceName={}, recordTime={}",
                    tFurnanceTemperature.getFurnanceName(), tFurnanceTemperature.getRecordTime());
         try {
             List<TFurnanceTemperature> list = tFurnanceTemperatureService.selectTFurnanceTemperatureList(tFurnanceTemperature);
@@ -1551,7 +1748,7 @@ public class TFurnanceTemperatureController extends BaseController
     @PostMapping
     public AjaxResult add(@RequestBody TFurnanceTemperature tFurnanceTemperature)
     {
-        logger.info("开始执行新增裂解炉炉管测温接口,入参: furnanceName={}, recordTime={}", 
+        logger.info("开始执行新增裂解炉炉管测温接口,入参: furnanceName={}, recordTime={}",
                    tFurnanceTemperature.getFurnanceName(), tFurnanceTemperature.getRecordTime());
         try {
             int result = tFurnanceTemperatureService.insertTFurnanceTemperature(tFurnanceTemperature);
@@ -1574,7 +1771,7 @@ public class TFurnanceTemperatureController extends BaseController
     @Log(title = "裂解炉炉管测温", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody TFurnanceTemperature tFurnanceTemperature) throws PinyinException {
-        logger.info("开始执行修改裂解炉炉管测温接口(APP),入参: id={}, furnanceName={}, recordTime={}", 
+        logger.info("开始执行修改裂解炉炉管测温接口(APP),入参: id={}, furnanceName={}, recordTime={}",
                    tFurnanceTemperature.getId(), tFurnanceTemperature.getFurnanceName(), tFurnanceTemperature.getRecordTime());
         try {
             String pass1 = tFurnanceTemperature.getPass1();
@@ -1935,6 +2132,146 @@ public class TFurnanceTemperatureController extends BaseController
         }
     }
 
+    /**
+     * 获取预测图表的历史数据
+     */
+    @PreAuthorize("@ss.hasPermi('production:temperature:list')")
+    @GetMapping("/coilPredictChartData")
+    public AjaxResult coilPredictChartData(TFurnanceTemperature tFurnanceTemperature) {
+        logger.info("开始获取预测图表历史数据 | furnace={} pass={}", tFurnanceTemperature.getFurnanceName(), tFurnanceTemperature.getPassNo());
+        try {
+            // 计算最近60天的日期范围
+            Calendar endCal = Calendar.getInstance();
+            Calendar startCal = Calendar.getInstance();
+            startCal.add(Calendar.DAY_OF_MONTH, -60);
+
+            String[] furnaces = {"H109", "H110", "H111", "H112", "H113", "H114", "H115", "H116", "H117", "H118", "H130"};
+            String furnace = tFurnanceTemperature.getFurnanceName();
+            String passNo = tFurnanceTemperature.getPassNo();
+
+            if (furnace == null || passNo == null) {
+                return AjaxResult.error("炉子和PASS号不能为空");
+            }
+
+            // 查询历史数据
+            TFurnanceTemperature query = new TFurnanceTemperature();
+            query.setFurnanceName(furnace);
+            query.setStartDate(startCal.getTime());
+            query.setEndDate(endCal.getTime());
+            List<TFurnanceTemperature> rawData = tFurnanceTemperatureService.selectTFurnanceTemperatureList(query);
+
+            // 按时间升序排序
+            rawData.sort(Comparator.comparing(TFurnanceTemperature::getRecordTime));
+
+            // 找到最近一次 status 为 1 或 2 的下标
+            int pivot = -1;
+            for (int i = 0; i < rawData.size(); i++) {
+                String status = rawData.get(i).getStatus();
+                if ("1".equals(status) || "2".equals(status)) {
+                    pivot = i;
+                }
+            }
+
+            // 只取 pivot 之后的所有数据
+            List<TFurnanceTemperature> filteredData = new ArrayList<>();
+            int startIndex = (pivot == -1) ? 0 : pivot + 1;
+            for (int i = startIndex; i < rawData.size(); i++) {
+                filteredData.add(rawData.get(i));
+            }
+
+            // 构建散点图数据
+            List<Map<String, Object>> chartData = new ArrayList<>();
+            SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");
+            int dayCount = 1;
+
+            for (TFurnanceTemperature item : filteredData) {
+                int value = 0;
+                String passKey = "pass" + passNo;
+
+                if ("H109".equals(furnace)) {
+                    value = setPassMaxValueH109(item, passKey);
+                } else if ("H130".equals(furnace)) {
+                    value = setPassMaxValueH130(item, passKey);
+                } else {
+                    value = setPassMaxValueH11x(item, passKey);
+                }
+
+                if (value > 0) { // 只包含有效数据
+                    Map<String, Object> dataPoint = new HashMap<>();
+                    dataPoint.put("day", dayCount);
+                    dataPoint.put("value", value);
+                    dataPoint.put("date", fmt.format(item.getRecordTime()));
+                    chartData.add(dataPoint);
+                    dayCount++;
+                }
+            }
+
+            // 计算线性回归斜率和方程
+            Map<String, String> regressionResult = null;
+            String slope = "0";
+            String equation = "y = 0x + 0";
+            String predictedDate = "-";
+            
+            if (chartData.size() >= 2) {
+                // 构建日期->值的映射用于线性回归
+                Map<String, Double> dateValueMap = new LinkedHashMap<>();
+                for (Map<String, Object> point : chartData) {
+                    // 安全地转换Integer到Double
+                    Object valueObj = point.get("value");
+                    Double value;
+                    if (valueObj instanceof Integer) {
+                        value = ((Integer) valueObj).doubleValue();
+                    } else if (valueObj instanceof Double) {
+                        value = (Double) valueObj;
+                    } else {
+                        value = Double.valueOf(valueObj.toString());
+                    }
+                    dateValueMap.put((String) point.get("date"), value);
+                }
+                
+                // 使用MathUtil进行线性回归
+                regressionResult = MathUtil.predictDateToReachLimit(dateValueMap, 1080.0);
+                if (regressionResult != null) {
+                    equation = regressionResult.get("equation");
+                    predictedDate = regressionResult.get("date");
+                    
+                    // 提取斜率
+                    try {
+                        slope = equation.split("x")[0].split("=")[1].trim();
+                    } catch (Exception e) {
+                        slope = "0";
+                    }
+                }
+            }
+
+            // 添加预测点位
+            Map<String, Object> predictPoint = new HashMap<>();
+            if (!"-".equals(predictedDate)) {
+                predictPoint.put("date", predictedDate);
+                predictPoint.put("value", 1080);
+                predictPoint.put("day", chartData.size() + 1); // 预测点位的天数
+                chartData.add(predictPoint);
+            }
+            
+            Map<String, Object> result = new HashMap<>();
+            result.put("furnace", furnace);
+            result.put("passNo", passNo);
+            result.put("chartData", chartData);
+            result.put("dataCount", chartData.size());
+            result.put("slope", slope);
+            result.put("equation", equation);
+            result.put("predictedDate", predictedDate);
+            result.put("predictPoint", predictPoint);
+
+            logger.info("coilPredictChartData completed | furnace={} pass={} dataPoints={}", furnace, passNo, chartData.size());
+            return AjaxResult.success(result);
+
+        } catch (Exception e) {
+            logger.error("获取预测图表历史数据异常", e);
+            return AjaxResult.error("获取数据失败: " + e.getMessage());
+        }
+    }
+
     public boolean isPassNegativeOrNull(String pass) {
         boolean flag = false;
         if (StringUtils.isNotNull(pass) && StringUtils.isNotEmpty(pass)) {

+ 6 - 8
master/src/main/java/com/ruoyi/project/training/controller/TTrainingRegularController.java

@@ -101,14 +101,12 @@ public class TTrainingRegularController extends BaseController
                 if (t.getDesignatedOther() != null) {
                     String[] designatedOther = t.getDesignatedOther().split(",");
                     for (String d : designatedOther) {
-                        if (d.equals("28")) {
-                            postStatu.set(postStatu.size() - 5, "(M)");
-                        }
-                        if (d.equals("30")) {
-                            postStatu.set(postStatu.size() - 4, "(M)");
-                        }
-                        if (d.equals("32")) {
-                            postStatu.set(postStatu.size() - 3, "(M)");
+                        // 根据岗位字典值找到对应的索引位置
+                        for (int i = 0; i < actualpost.size(); i++) {
+                            if (actualpost.get(i).getDictValue().equals(d)) {
+                                postStatu.set(i, "(M)");
+                                break;
+                            }
                         }
                     }
                 }

+ 8 - 6
master/src/main/resources/mybatis/production/TFurnanceTemperatureMapper.xml

@@ -3,7 +3,7 @@
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.ruoyi.project.production.mapper.TFurnanceTemperatureMapper">
-    
+
     <resultMap type="TFurnanceTemperature" id="TFurnanceTemperatureResult">
         <result property="id"    column="id"    />
         <result property="furnanceName"    column="furnance_name"    />
@@ -43,11 +43,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectTFurnanceTemperatureList" parameterType="TFurnanceTemperature" resultMap="TFurnanceTemperatureResult">
         <include refid="selectTFurnanceTemperatureVo"/>
-        <where>  
+        <where>
             <if test="furnanceName != null  and furnanceName != ''"> and furnance_name = #{furnanceName}</if>
             <if test="recordTime != null ">
                 and trunc(record_time, 'MM') = #{recordTime}
             </if>
+            <if test="startDate != null "> and record_time &gt;= #{startDate}</if>
+            <if test="endDate != null "> and record_time &lt;= #{endDate}</if>
             <if test="pass1 != null  and pass1 != ''"> and pass1 = #{pass1}</if>
             <if test="pass2 != null  and pass2 != ''"> and pass2 = #{pass2}</if>
             <if test="pass3 != null  and pass3 != ''"> and pass3 = #{pass3}</if>
@@ -94,7 +96,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <!-- 数据范围过滤 -->
         ${params.dataScope}
     </select>
-    
+
     <select id="selectTFurnanceTemperatureById" parameterType="Long" resultMap="TFurnanceTemperatureResult">
         <include refid="selectTFurnanceTemperatureVo"/>
         where id = #{id}
@@ -107,7 +109,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         and d.record_time &lt;= #{endDate}
         order by record_time asc
     </select>
-        
+
     <insert id="insertTFurnanceTemperature" parameterType="TFurnanceTemperature">
         <selectKey keyProperty="id" resultType="long" order="BEFORE">
             SELECT seq_t_furnance_temperature.NEXTVAL as id FROM DUAL
@@ -216,5 +218,5 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{id}
         </foreach>
     </update>
-    
-</mapper>
+
+</mapper>

+ 18 - 0
ui/src/api/production/temperature.js

@@ -35,6 +35,15 @@ export function listCoil(query) {
   })
 }
 
+// 预测各炉各PASS达到1080的日期(最近60天)
+export function listCoilPredict1080(query) {
+  return request({
+    url: '/production/temperature/coilPredict1080',
+    method: 'get',
+    params: query
+  })
+}
+
 // 查询裂解炉炉管测温COIL趋势分析
 export function listCoilAnalysis(query) {
   return request({
@@ -95,3 +104,12 @@ export function exportTemperature(query) {
     params: query
   })
 }
+
+// 查询预测图表历史数据
+export function listCoilPredictChartData(query) {
+  return request({
+    url: '/production/temperature/coilPredictChartData',
+    method: 'get',
+    params: query
+  })
+}

+ 4 - 4
ui/src/views/plant/EOEGorganization/index.vue

@@ -252,8 +252,8 @@ export default {
                   label: this.staffmgrList[i].name,
                   post: post,
                   secretary: [[], []],
-                  // img: 'http://47.114.101.16:8080' + process.env.VUE_APP_BASE_API + this.staffmgrList[i].photo,
-                  img: 'https://cpms.basf-ypc.net.cn' + process.env.VUE_APP_BASE_API + this.staffmgrList[i].photo,
+                  img: 'http://47.114.101.16' + process.env.VUE_APP_BASE_API + this.staffmgrList[i].photo,
+                  // img: 'https://cpms.basf-ypc.net.cn' + process.env.VUE_APP_BASE_API + this.staffmgrList[i].photo,
                   bz1: false,
                   bz2: false,
                   bz3: false,
@@ -302,8 +302,8 @@ export default {
                   pId: this.staffmgrList[i].pId,
                   label: this.staffmgrList[i].name,
                   post: post,
-                  img: 'https://cpms.basf-ypc.net.cn' + process.env.VUE_APP_BASE_API + this.staffmgrList[i].photo,
-                  // img: 'http://47.114.101.16:8080' + process.env.VUE_APP_BASE_API + this.staffmgrList[i].photo,
+                  // img: 'https://cpms.basf-ypc.net.cn' + process.env.VUE_APP_BASE_API + this.staffmgrList[i].photo,
+                  img: 'http://47.114.101.16' + process.env.VUE_APP_BASE_API + this.staffmgrList[i].photo,
                   bz1: false,
                   bz2: false,
                   bz3: false,

+ 410 - 43
ui/src/views/production/temperature/coil.vue

@@ -15,101 +15,136 @@
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
       </el-form-item>
     </el-form>
-    <el-table v-loading="loading" :data="temperatureList" @selection-change="handleSelectionChange" :height="clientHeight" border>
+    <el-table v-loading="loading" :data="mergedTableData" @selection-change="handleSelectionChange" :height="clientHeight" border>
       <el-table-column label="巡检日期" align="center" prop="recordTime" width="100">
         <template slot-scope="scope">
-          <span>{{ parseTime(scope.row.recordTime, '{y}-{m}-{d}') }}</span>
+          <span v-if="scope.row.isPredictRow" style="font-weight: bold; color: #409EFF;">{{ scope.row.recordTime }}</span>
+          <span v-else>{{ parseTime(scope.row.recordTime, '{y}-{m}-{d}') }}</span>
         </template>
       </el-table-column>
       <el-table-column label="H109 OUT" align="center" prop="h109Out" :show-overflow-tooltip="true">
-        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="80">
+        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="83">
           <template slot-scope="scope">
-            <span @click="handleCoilAnalysis('H109', (index+1).toString())" v-if="scope.row.h109Out[index]>=1080" style="color:red;">{{scope.row.h109Out[index]}}</span>
-            <span @click="handleCoilAnalysis('H109', (index+1).toString())" v-if="scope.row.h109Out[index]<1080">{{scope.row.h109Out[index]}}</span>
+            <span v-if="scope.row.isPredictRow" style="color: #409EFF; font-weight: bold; cursor: pointer;" @click="handlePredictAnalysis('H109', (index+1).toString())">{{scope.row.h109Out[index] || '-'}}</span>
+            <span v-else>
+              <span @click="handleCoilAnalysis('H109', (index+1).toString())" v-if="scope.row.h109Out[index]>=1080" style="color:red;">{{scope.row.h109Out[index]}}</span>
+              <span @click="handleCoilAnalysis('H109', (index+1).toString())" v-if="scope.row.h109Out[index]<1080">{{scope.row.h109Out[index]}}</span>
+            </span>
           </template>
         </el-table-column>
       </el-table-column>
       <el-table-column label="H110 OUT" align="center" prop="h110Out" :show-overflow-tooltip="true">
-        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="80">
+        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="83">
           <template slot-scope="scope">
-            <span @click="handleCoilAnalysis('H110', (index+1).toString())" v-if="scope.row.h110Out[index]>=1080" style="color:red;">{{scope.row.h110Out[index]}}</span>
-            <span @click="handleCoilAnalysis('H110', (index+1).toString())" v-if="scope.row.h110Out[index]<1080">{{scope.row.h110Out[index]}}</span>
+            <span v-if="scope.row.isPredictRow" style="color: #409EFF; font-weight: bold; cursor: pointer;" @click="handlePredictAnalysis('H110', (index+1).toString())">{{scope.row.h110Out[index] || '-'}}</span>
+            <span v-else>
+              <span @click="handleCoilAnalysis('H110', (index+1).toString())" v-if="scope.row.h110Out[index]>=1080" style="color:red;">{{scope.row.h110Out[index]}}</span>
+              <span @click="handleCoilAnalysis('H110', (index+1).toString())" v-if="scope.row.h110Out[index]<1080">{{scope.row.h110Out[index]}}</span>
+            </span>
           </template>
         </el-table-column>
       </el-table-column>
       <el-table-column label="H111 OUT" align="center" prop="h111Out" :show-overflow-tooltip="true">
-        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="80">
+        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="83">
           <template slot-scope="scope">
-            <span @click="handleCoilAnalysis('H111', (index+1).toString())" v-if="scope.row.h111Out[index]>=1080" style="color:red;">{{scope.row.h111Out[index]}}</span>
-            <span @click="handleCoilAnalysis('H111', (index+1).toString())" v-if="scope.row.h111Out[index]<1080">{{scope.row.h111Out[index]}}</span>
+            <span v-if="scope.row.isPredictRow" style="color: #409EFF; font-weight: bold; cursor: pointer;" @click="handlePredictAnalysis('H111', (index+1).toString())">{{scope.row.h111Out[index] || '-'}}</span>
+            <span v-else>
+              <span @click="handleCoilAnalysis('H111', (index+1).toString())" v-if="scope.row.h111Out[index]>=1080" style="color:red;">{{scope.row.h111Out[index]}}</span>
+              <span @click="handleCoilAnalysis('H111', (index+1).toString())" v-if="scope.row.h111Out[index]<1080">{{scope.row.h111Out[index]}}</span>
+            </span>
           </template>
         </el-table-column>
       </el-table-column>
       <el-table-column label="H112 OUT" align="center" prop="h112Out" :show-overflow-tooltip="true">
-        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="80">
+        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="83">
           <template slot-scope="scope">
-            <span @click="handleCoilAnalysis('H112', (index+1).toString())" v-if="scope.row.h112Out[index]>=1080" style="color:red;">{{scope.row.h112Out[index]}}</span>
-            <span @click="handleCoilAnalysis('H112', (index+1).toString())" v-if="scope.row.h112Out[index]<1080">{{scope.row.h112Out[index]}}</span>
+            <span v-if="scope.row.isPredictRow" style="color: #409EFF; font-weight: bold; cursor: pointer;" @click="handlePredictAnalysis('H112', (index+1).toString())">{{scope.row.h112Out[index] || '-'}}</span>
+            <span v-else>
+              <span @click="handleCoilAnalysis('H112', (index+1).toString())" v-if="scope.row.h112Out[index]>=1080" style="color:red;">{{scope.row.h112Out[index]}}</span>
+              <span @click="handleCoilAnalysis('H112', (index+1).toString())" v-if="scope.row.h112Out[index]<1080">{{scope.row.h112Out[index]}}</span>
+            </span>
           </template>
         </el-table-column>
       </el-table-column>
       <el-table-column label="H113 OUT" align="center" prop="h113Out" :show-overflow-tooltip="true">
-        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="80">
+        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="83">
           <template slot-scope="scope">
-            <span @click="handleCoilAnalysis('H113', (index+1).toString())" v-if="scope.row.h113Out[index]>=1080" style="color:red;">{{scope.row.h113Out[index]}}</span>
-            <span @click="handleCoilAnalysis('H113', (index+1).toString())" v-if="scope.row.h113Out[index]<1080">{{scope.row.h113Out[index]}}</span>
+            <span v-if="scope.row.isPredictRow" style="color: #409EFF; font-weight: bold; cursor: pointer;" @click="handlePredictAnalysis('H113', (index+1).toString())">{{scope.row.h113Out[index] || '-'}}</span>
+            <span v-else>
+              <span @click="handleCoilAnalysis('H113', (index+1).toString())" v-if="scope.row.h113Out[index]>=1080" style="color:red;">{{scope.row.h113Out[index]}}</span>
+              <span @click="handleCoilAnalysis('H113', (index+1).toString())" v-if="scope.row.h113Out[index]<1080">{{scope.row.h113Out[index]}}</span>
+            </span>
           </template>
         </el-table-column>
       </el-table-column>
       <el-table-column label="H114 OUT" align="center" prop="h114Out" :show-overflow-tooltip="true">
-        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="80">
+        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="83">
           <template slot-scope="scope">
-            <span @click="handleCoilAnalysis('H114', (index+1).toString())" v-if="scope.row.h114Out[index]>=1080" style="color:red;">{{scope.row.h114Out[index]}}</span>
-            <span @click="handleCoilAnalysis('H114', (index+1).toString())" v-if="scope.row.h114Out[index]<1080">{{scope.row.h114Out[index]}}</span>
+            <span v-if="scope.row.isPredictRow" style="color: #409EFF; font-weight: bold; cursor: pointer;" @click="handlePredictAnalysis('H114', (index+1).toString())">{{scope.row.h114Out[index] || '-'}}</span>
+            <span v-else>
+              <span @click="handleCoilAnalysis('H114', (index+1).toString())" v-if="scope.row.h114Out[index]>=1080" style="color:red;">{{scope.row.h114Out[index]}}</span>
+              <span @click="handleCoilAnalysis('H114', (index+1).toString())" v-if="scope.row.h114Out[index]<1080">{{scope.row.h114Out[index]}}</span>
+            </span>
           </template>
         </el-table-column>
       </el-table-column>
       <el-table-column label="H115 OUT" align="center" prop="h115Out" :show-overflow-tooltip="true">
-        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="80">
+        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="83">
           <template slot-scope="scope">
-            <span @click="handleCoilAnalysis('H115', (index+1).toString())" v-if="scope.row.h115Out[index]>=1080" style="color:red;">{{scope.row.h115Out[index]}}</span>
-            <span @click="handleCoilAnalysis('H115', (index+1).toString())" v-if="scope.row.h115Out[index]<1080">{{scope.row.h115Out[index]}}</span>
+            <span v-if="scope.row.isPredictRow" style="color: #409EFF; font-weight: bold; cursor: pointer;" @click="handlePredictAnalysis('H115', (index+1).toString())">{{scope.row.h115Out[index] || '-'}}</span>
+            <span v-else>
+              <span @click="handleCoilAnalysis('H115', (index+1).toString())" v-if="scope.row.h115Out[index]>=1080" style="color:red;">{{scope.row.h115Out[index]}}</span>
+              <span @click="handleCoilAnalysis('H115', (index+1).toString())" v-if="scope.row.h115Out[index]<1080">{{scope.row.h115Out[index]}}</span>
+            </span>
           </template>
         </el-table-column>
       </el-table-column>
       <el-table-column label="H116 OUT" align="center" prop="h116Out" :show-overflow-tooltip="true">
-        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="80">
+        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="83">
           <template slot-scope="scope">
-            <span @click="handleCoilAnalysis('H116', (index+1).toString())" v-if="scope.row.h116Out[index]>=1080" style="color:red;">{{scope.row.h116Out[index]}}</span>
-            <span @click="handleCoilAnalysis('H116', (index+1).toString())" v-if="scope.row.h116Out[index]<1080">{{scope.row.h116Out[index]}}</span>
+            <span v-if="scope.row.isPredictRow" style="color: #409EFF; font-weight: bold; cursor: pointer;" @click="handlePredictAnalysis('H116', (index+1).toString())">{{scope.row.h116Out[index] || '-'}}</span>
+            <span v-else>
+              <span @click="handleCoilAnalysis('H116', (index+1).toString())" v-if="scope.row.h116Out[index]>=1080" style="color:red;">{{scope.row.h116Out[index]}}</span>
+              <span @click="handleCoilAnalysis('H116', (index+1).toString())" v-if="scope.row.h116Out[index]<1080">{{scope.row.h116Out[index]}}</span>
+            </span>
           </template>
         </el-table-column>
       </el-table-column>
       <el-table-column label="H117 OUT" align="center" prop="h117Out" :show-overflow-tooltip="true">
-        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="80">
+        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="83">
           <template slot-scope="scope">
-            <span @click="handleCoilAnalysis('H117', (index+1).toString())" v-if="scope.row.h117Out[index]>=1080" style="color:red;">{{scope.row.h117Out[index]}}</span>
-            <span @click="handleCoilAnalysis('H117', (index+1).toString())" v-if="scope.row.h117Out[index]<1080">{{scope.row.h117Out[index]}}</span>
+            <span v-if="scope.row.isPredictRow" style="color: #409EFF; font-weight: bold; cursor: pointer;" @click="handlePredictAnalysis('H117', (index+1).toString())">{{scope.row.h117Out[index] || '-'}}</span>
+            <span v-else>
+              <span @click="handleCoilAnalysis('H117', (index+1).toString())" v-if="scope.row.h117Out[index]>=1080" style="color:red;">{{scope.row.h117Out[index]}}</span>
+              <span @click="handleCoilAnalysis('H117', (index+1).toString())" v-if="scope.row.h117Out[index]<1080">{{scope.row.h117Out[index]}}</span>
+            </span>
           </template>
         </el-table-column>
       </el-table-column>
       <el-table-column label="H118 OUT" align="center" prop="h118Out" :show-overflow-tooltip="true">
-        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="80">
+        <el-table-column v-for="(item,index) in 8" :label="'PASS '+(index+1).toString()" align="center" width="83">
           <template slot-scope="scope">
-            <span @click="handleCoilAnalysis('H118', (index+1).toString())" v-if="scope.row.h118Out[index]>=1080" style="color:red;">{{scope.row.h118Out[index]}}</span>
-            <span @click="handleCoilAnalysis('H118', (index+1).toString())" v-if="scope.row.h118Out[index]<1080">{{scope.row.h118Out[index]}}</span>
+            <span v-if="scope.row.isPredictRow" style="color: #409EFF; font-weight: bold; cursor: pointer;" @click="handlePredictAnalysis('H118', (index+1).toString())">{{scope.row.h118Out[index] || '-'}}</span>
+            <span v-else>
+              <span @click="handleCoilAnalysis('H118', (index+1).toString())" v-if="scope.row.h118Out[index]>=1080" style="color:red;">{{scope.row.h118Out[index]}}</span>
+              <span @click="handleCoilAnalysis('H118', (index+1).toString())" v-if="scope.row.h118Out[index]<1080">{{scope.row.h118Out[index]}}</span>
+            </span>
           </template>
         </el-table-column>
       </el-table-column>
       <el-table-column label="H130 OUT" align="center" prop="h130Out" :show-overflow-tooltip="true">
-        <el-table-column v-for="(item,index) in 12" :label="'PASS '+(index+1).toString()" align="center" width="80">
+        <el-table-column v-for="(item,index) in 12" :label="'PASS '+(index+1).toString()" align="center" width="83">
           <template slot-scope="scope">
-            <span @click="handleCoilAnalysis('H130', (index+1).toString())" v-if="scope.row.h130Out[index]>=1080" style="color:red;">{{scope.row.h130Out[index]}}</span>
-            <span @click="handleCoilAnalysis('H130', (index+1).toString())" v-if="scope.row.h130Out[index]<1080">{{scope.row.h130Out[index]}}</span>
+            <span v-if="scope.row.isPredictRow" style="color: #409EFF; font-weight: bold; cursor: pointer;" @click="handlePredictAnalysis('H130', (index+1).toString())">{{scope.row.h130Out[index] || '-'}}</span>
+            <span v-else>
+              <span @click="handleCoilAnalysis('H130', (index+1).toString())" v-if="scope.row.h130Out[index]>=1080" style="color:red;">{{scope.row.h130Out[index]}}</span>
+              <span @click="handleCoilAnalysis('H130', (index+1).toString())" v-if="scope.row.h130Out[index]<1080">{{scope.row.h130Out[index]}}</span>
+            </span>
           </template>
         </el-table-column>
       </el-table-column>
     </el-table>
+
     <!-- COIL趋势分析对话框 -->
     <el-dialog  :close-on-click-modal="false" @close="disposeChart" :title="analysis.title" :visible.sync="analysis.open" width="80%" append-to-body>
       <el-form :model="analysisQueryParams" ref="queryForm" :inline="true" label-width="68px">
@@ -135,11 +170,30 @@
       </el-form>
       <div id="trendChartTemperature" style="width:100%; height: 600px;"></div>
     </el-dialog>
+
+    <!-- 预测分析对话框 -->
+    <el-dialog :close-on-click-modal="false" @close="disposePredictChart" :title="predictAnalysis.title" :visible.sync="predictAnalysis.open" width="80%" append-to-body>
+          <div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 20px;">
+            <div style="flex: 1; min-width: 200px;">
+              <span style="color: #606266; font-size: 14px;">线性回归方程:</span>
+              <span style="color: #303133; font-weight: bold;">{{ predictChartData.equation }}</span>
+            </div>
+            <div style="flex: 1; min-width: 120px;">
+              <span style="color: #606266; font-size: 14px;">斜率:</span>
+              <span style="color: #E6A23C; font-weight: bold;">{{ predictChartData.slope }}</span>
+            </div>
+            <div style="flex: 1; min-width: 200px;">
+              <span style="color: #606266; font-size: 14px;">预测达到1080的日期:</span>
+              <span style="color: #F56C6C; font-weight: bold;">{{ predictChartData.predictedDate || '-' }}</span>
+            </div>
+          </div>
+      <div id="predictChartTemperature" style="width:100%; height: 600px;"></div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-  import { listCoil, listCoilAnalysis } from "@/api/production/temperature";
+  import { listCoil, listCoilAnalysis, listCoilPredict1080, listCoilPredictChartData } from "@/api/production/temperature";
   import { treeselect } from "@/api/system/dept";
   import { getToken } from "@/utils/auth";
   import Treeselect from "@riophae/vue-treeselect";
@@ -169,13 +223,6 @@
         },
         // 趋势图
         chart: null,
-        // 对话框控件值
-        analysisDialogElement: {
-          // 开始时间
-          startDate: null,
-          // 结束时间
-          endDate: null,
-        },
         // 趋势分析参数
         analysis: {
           // 是否显示弹出层
@@ -183,6 +230,25 @@
           // 弹出层标题
           title: "",
         },
+        // 预测分析参数
+        predictAnalysis: {
+          // 是否显示弹出层
+          open: false,
+          // 弹出层标题
+          title: "",
+        },
+        // 预测图表
+        predictChart: null,
+        // 预测图表数据
+        predictChartData: {
+          scatterData: [],
+          historicalData: [],
+          predictData: [],
+          lineData: [],
+          equation: "",
+          predictedDate: "",
+          slope: ""
+        },
         // 遮罩层
         loading: true,
         // 选中数组
@@ -197,11 +263,16 @@
         total: 0,
         // 裂解炉炉管测温表格数据
         temperatureList: [],
+        // 合并后的表格数据(包含当前数据和预测数据行)
+        mergedTableData: [],
         // 弹出层标题
         title: "",
         // 部门树选项
         deptOptions: undefined,
         clientHeight:300,
+        // 预测表(结构与上表一致)
+        loadingPredict: false,
+        predictList: [],
         // 是否显示弹出层
         open: false,
         // 用户导入参数
@@ -277,6 +348,7 @@
         this.clientHeight = document.body.clientHeight -250
       })
       // this.getList();
+      // this.getPredict();
       // this.getTreeselect();
     },
     methods: {
@@ -368,10 +440,238 @@
         this.analysis.open = true;
         this.draw();
       },
+      /** 处理预测分析点击事件 */
+      handlePredictAnalysis(furnace, passNo) {
+        this.predictAnalysis.title = "预测分析(" + furnace + " PASS " + passNo + ")";
+        this.predictAnalysis.open = true;
+
+        // 直接调用coilPredictChartData接口获取数据
+        listCoilPredictChartData({ furnanceName: furnace, passNo: passNo }).then(response => {
+          if (response.data) {
+            const chartData = response.data;
+
+            // 分离历史数据点和预测点
+            const historicalData = [];
+            const predictPoints = [];
+
+            chartData.chartData.forEach(item => {
+              if (item.value === 1080 && chartData.predictedDate && item.date === chartData.predictedDate) {
+                predictPoints.push([item.date, item.value]);
+              } else {
+                historicalData.push([item.date, item.value]);
+              }
+            });
+
+            this.predictChartData = {
+              scatterData: chartData.chartData.map(item => [item.date, item.value]),
+              historicalData: historicalData,
+              predictData: predictPoints,
+              equation: chartData.equation || "y = 0x + 0",
+              predictedDate: chartData.predictedDate || "-",
+              slope: chartData.slope || "0"
+            };
+
+            this.$nextTick(() => this.drawPredictChart());
+          } else {
+            this.$message.warning("暂无历史数据");
+          }
+        }).catch(() => {
+          this.$message.error("获取历史数据失败");
+        });
+      },
+      /** 生成模拟预测数据(实际应该从后端获取) */
+      /** 绘制预测图表 */
+      drawPredictChart() {
+        this.predictChart = this.echarts.init(document.getElementById('predictChartTemperature'));
+
+        // 辅助函数:格式化日期
+        const formatDate = (date) => {
+          const year = date.getFullYear();
+          const month = String(date.getMonth() + 1).padStart(2, '0');
+          const day = String(date.getDate()).padStart(2, '0');
+          return `${year}-${month}-${day}`;
+        };
+
+        // 辅助函数:生成完整时间范围
+        const generateFullDateRange = (startDate, endDate) => {
+          const dates = [];
+          const start = new Date(startDate);
+          const end = new Date(endDate);
+          const current = new Date(start);
+          while (current <= end) {
+            dates.push(formatDate(current));
+            current.setDate(current.getDate() + 1);
+          }
+          return dates;
+        };
+
+        // 辅助函数:从方程提取截距
+        const extractIntercept = (equation) => {
+          try {
+            const match = equation.match(/y\s*=\s*([+-]?\d*\.?\d*)x\s*([+-]\s*\d*\.?\d*)/);
+            if (match) return parseFloat(match[2].replace(/\s+/g, ''));
+            const simpleMatch = equation.match(/[+-]\s*\d*\.?\d*$/);
+            if (simpleMatch) return parseFloat(simpleMatch[0].replace(/\s+/g, ''));
+          } catch (e) {
+            console.warn('解析方程失败:', equation, e);
+          }
+          return 0;
+        };
+
+        // 计算Y轴范围
+        const allValues = this.predictChartData.scatterData.map(item => item[1]);
+        const minValue = Math.min(...allValues);
+        const maxValue = Math.max(...allValues);
+        const yAxisMin = Math.max(0, minValue - (maxValue - minValue) * 0.1);
+        const yAxisMax = Math.max(1080, maxValue + (maxValue - minValue) * 0.1);
+
+        // 计算时间范围
+        const dates = this.predictChartData.scatterData.map(item => item[0]).sort();
+        const minDate = dates[0];
+        const maxDate = dates[dates.length - 1];
+        const endDate = this.predictChartData.predictedDate && this.predictChartData.predictedDate !== '-'
+          ? this.predictChartData.predictedDate : maxDate;
+
+        // 生成完整时间轴
+        const fullDateRange = generateFullDateRange(minDate, endDate);
+
+        // 计算回归线端点
+        const slope = parseFloat(this.predictChartData.slope) || 0;
+        const intercept = extractIntercept(this.predictChartData.equation || "y = 0x + 0");
+        const minDay = Math.floor((new Date(minDate) - new Date(minDate)) / (1000 * 60 * 60 * 24)) + 1;
+        const endDay = Math.floor((new Date(endDate) - new Date(minDate)) / (1000 * 60 * 60 * 24)) + 1;
+        const startValue = slope * minDay + intercept;
+        const endValue = slope * endDay + intercept;
+
+        // 构建markLine配置
+        const markLineOpt = {
+          animation: false,
+          label: {
+            formatter: this.predictChartData.equation,
+            align: 'right',
+            fontSize: 12,
+            color: '#E6A23C'
+          },
+          lineStyle: {
+            type: 'solid',
+            color: '#E6A23C',
+            width: 2
+          },
+          tooltip: {
+            formatter: `回归方程: ${this.predictChartData.equation}<br/>斜率: ${this.predictChartData.slope}`
+          },
+          data: [
+            [
+              {
+                coord: [minDate, startValue],
+                symbol: 'none'
+              },
+              {
+                coord: [endDate, endValue],
+                symbol: 'none'
+              }
+            ]
+          ]
+        };
+
+        const option = {
+          title: {
+            text: 'COIL温度预测分析',
+            left: 'center'
+          },
+          tooltip: {
+            trigger: 'item',
+            formatter: (params) => {
+              if (params.seriesName === '历史数据') {
+                return `日期: ${params.data[0]}<br/>温度: ${params.data[1]}°C`;
+              }
+            }
+          },
+          legend: {
+            data: ['历史数据', '预测点'],
+            top: 30
+          },
+          xAxis: {
+            type: 'category',
+            name: '日期',
+            nameLocation: 'middle',
+            nameGap: 30,
+            data: fullDateRange, // 使用完整的时间范围
+            axisLabel: {
+              rotate: 45,
+              formatter: function(value) {
+                // 只显示月-日,避免标签重叠
+                return value.substring(5);
+              },
+              interval: 'auto', // 自动调整标签间隔,避免重叠
+              margin: 10 // 增加标签与轴线的距离
+            },
+            axisTick: {
+              alignWithLabel: true // 刻度线与标签对齐
+            },
+            splitLine: {
+              show: false // 隐藏分割线
+            }
+          },
+          yAxis: {
+            type: 'value',
+            name: '温度(°C)',
+            nameLocation: 'middle',
+            nameGap: 50,
+            min: yAxisMin,
+            max: yAxisMax
+          },
+          series: [
+            {
+              name: '历史数据',
+              type: 'scatter',
+              data: this.predictChartData.historicalData,
+              symbolSize: 8,
+              itemStyle: {
+                color: '#409EFF'
+              },
+              markLine: markLineOpt
+            },
+            {
+              name: '预测点',
+              type: 'scatter',
+              data: this.predictChartData.predictData,
+              symbolSize: 12,
+              itemStyle: {
+                color: '#F56C6C'
+              },
+              tooltip: {
+                formatter: (params) => {
+                  return `预测日期: ${params.data[0]}<br/>预测温度: ${params.data[1]}°C<br/>预测达到1080°C`;
+                }
+              }
+            }
+          ]
+        };
+
+        this.predictChart.setOption(option);
+      },
+      /** 销毁预测图表 */
+      disposePredictChart() {
+        if (this.predictChart) {
+          this.echarts.dispose(this.predictChart);
+          this.predictChart = null;
+        }
+        this.predictChartData = {
+          scatterData: [],
+          historicalData: [],
+          predictData: [],
+          lineData: [],
+          equation: "",
+          predictedDate: "",
+          slope: ""
+        };
+      },
       init() {
         let date = new Date();
         this.queryParams.recordTime = date.getFullYear() + "-" + Number(date.getMonth() + 1);
         this.getList();
+        this.getPredict();
       },
       /** 查询裂解炉炉管测温列表 */
       getList() {
@@ -395,10 +695,76 @@
               if (response.data[i].h130Out == null) { response.data[i].h130Out = []; } else { response.data[i].h130Out = response.data[i].h130Out.split(','); }
             }
             this.temperatureList = response.data;
+            // 合并数据
+            this.mergeTableData();
           }
           this.loading = false;
         });
       },
+      // 获取预测表数据
+      async getPredict() {
+        this.loadingPredict = true;
+        try {
+          const res = await listCoilPredict1080({});
+          const list = res && res.data ? res.data : [];
+          if (!list.length) {
+            this.predictList = [];
+            return;
+          }
+          const rec = { ...list[0] };
+          const toArr = v => (v ? ('' + v).split(',') : []);
+          rec.h109Out = toArr(rec.h109Out);
+          rec.h110Out = toArr(rec.h110Out);
+          rec.h111Out = toArr(rec.h111Out);
+          rec.h112Out = toArr(rec.h112Out);
+          rec.h113Out = toArr(rec.h113Out);
+          rec.h114Out = toArr(rec.h114Out);
+          rec.h115Out = toArr(rec.h115Out);
+          rec.h116Out = toArr(rec.h116Out);
+          rec.h117Out = toArr(rec.h117Out);
+          rec.h118Out = toArr(rec.h118Out);
+          rec.h130Out = toArr(rec.h130Out);
+          // recordTime 为日期字符串或时间戳,直接显示
+          this.predictList = [rec];
+          // 合并数据
+          this.mergeTableData();
+        } catch (e) {
+          this.predictList = [];
+        } finally {
+          this.loadingPredict = false;
+        }
+      },
+      /** 合并表格数据 - 将预测数据作为最后一行添加 */
+      mergeTableData() {
+        // 先复制当前数据
+        const mergedData = [...this.temperatureList];
+
+        // 如果有预测数据,将其作为最后一行添加
+        if (this.predictList.length > 0) {
+          const predictData = this.predictList[0];
+
+          // 创建预测行数据,结构与当前数据一致
+          const predictRow = {
+            recordTime: '预测1080日期', // 特殊标识
+            h109Out: predictData.h109Out || [],
+            h110Out: predictData.h110Out || [],
+            h111Out: predictData.h111Out || [],
+            h112Out: predictData.h112Out || [],
+            h113Out: predictData.h113Out || [],
+            h114Out: predictData.h114Out || [],
+            h115Out: predictData.h115Out || [],
+            h116Out: predictData.h116Out || [],
+            h117Out: predictData.h117Out || [],
+            h118Out: predictData.h118Out || [],
+            h130Out: predictData.h130Out || [],
+            isPredictRow: true // 标记为预测行
+          };
+
+          mergedData.push(predictRow);
+        }
+
+        this.mergedTableData = mergedData;
+      },
       /** 查询部门下拉树结构 */
       getTreeselect() {
         treeselect().then(response => {
@@ -461,6 +827,7 @@
       handleQuery() {
         this.queryParams.pageNum = 1;
         this.getList();
+        this.getPredict();
       },
       /** 重置按钮操作 */
       resetQuery() {

+ 1 - 1
ui/src/views/training/companylevel/index.vue

@@ -125,7 +125,7 @@
       <el-table-column :label="$t('课程名称')" align="center" prop="item" width="500" :show-overflow-tooltip="true"/>
       <el-table-column :label="$t('频率')" align="center" prop="frequency" width="100" :show-overflow-tooltip="true" >
         <template slot-scope="scope">
-          <span v-if="scope.row.frequency !== null">{{scope.row.frequency}}年一次</span>
+          <span v-if="scope.row.frequency !== null">{{scope.row.frequency}}</span>
           <span v-else>一次</span>
         </template>
       </el-table-column>