ly 20 часов назад
Родитель
Сommit
3aba08a476

+ 139 - 0
master/src/main/java/com/ruoyi/project/reliability/controller/TRelDeviceController.java

@@ -5,6 +5,8 @@ import java.io.IOException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.Set;
 
 import com.alibaba.fastjson.JSON;
 import com.ruoyi.common.utils.file.ExcelUtils;
@@ -14,8 +16,11 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 import com.ruoyi.framework.aspectj.lang.annotation.Log;
 import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
+import com.ruoyi.project.reliability.domain.TRelCompo;
 import com.ruoyi.project.reliability.domain.TRelDevice;
+import com.ruoyi.project.reliability.service.ITRelCompoService;
 import com.ruoyi.project.reliability.service.ITRelDeviceService;
+import com.ruoyi.project.reliability.utils.MaintFrequencyUtils;
 import com.ruoyi.framework.web.controller.BaseController;
 import com.ruoyi.framework.web.domain.AjaxResult;
 import com.ruoyi.common.utils.poi.ExcelUtil;
@@ -37,6 +42,9 @@ public class TRelDeviceController extends BaseController
     @Autowired
     private ITRelDeviceService tRelDeviceService;
 
+    @Autowired
+    private ITRelCompoService tRelCompoService;
+
     @Autowired
     private ISysUserService userService;
 
@@ -75,6 +83,137 @@ public class TRelDeviceController extends BaseController
         return AjaxResult.success(tRelDeviceService.selectTRelDeviceById(devId));
     }
 
+    /**
+     * 查询指定设备(devTag)下“已延期/逾期”的部件名称列表。
+     *
+     * 延期判断规则:
+     * - 只按“上一次日期 + 频率”计算下次日期
+     * - 如果某个类型没有上一次日期,则该类型不参与延期计算(不兜底用 1 月 1 日等基准)
+     * - 只要检查/维修/更换任意一种类型计算后已过期,则该部件判定为“延期部件”
+     */
+    @PreAuthorize("@ss.hasPermi('reliability:rel_device:query')")
+    @GetMapping(value = "/overdueCompos")
+    public AjaxResult listOverdueCompos(@RequestParam("devTag") String devTag)
+    {
+        if (StringUtils.isEmpty(devTag)) {
+            return AjaxResult.error("devTag不能为空");
+        }
+
+        List<TRelCompo> compoList = tRelCompoService.selectTRelCompoByDevTag(devTag);
+        if (compoList == null || compoList.isEmpty()) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+
+        Set<String> overdueCompoNames = new LinkedHashSet<>();
+        for (TRelCompo compo : compoList) {
+            if (compo == null || StringUtils.isEmpty(compo.getCompoName())) {
+                continue;
+            }
+
+            boolean inspOverdue = isOverdueByLastAndFreq(compo.getLastInspDate(), compo.getInspFreq());
+            boolean fixOverdue = isOverdueByLastAndFreq(compo.getLastFixDate(), compo.getFixFreq());
+            boolean replaceOverdue = isOverdueByLastAndFreq(compo.getLastReplaceDate(), compo.getReplaceFreq());
+
+            if (inspOverdue || fixOverdue || replaceOverdue) {
+                overdueCompoNames.add(compo.getCompoName());
+            }
+        }
+
+        return AjaxResult.success(new ArrayList<>(overdueCompoNames));
+    }
+
+    /**
+     * 查询指定设备(devTag)下“即将到期”的部件名称列表。
+     *
+     * 即将到期判断规则:
+     * - 计算方式同延期:只按“上一次日期 + 频率”计算下次日期
+     * - 没有“上一次日期”的不参与计算
+     * - 周期的最后20%以内:距下次到期日的剩余天数 <= 周期天数 * 20%
+     * - 但不能延期:若该部件任一类型(检查/维修/更换)已逾期,则该部件不纳入“即将到期”
+     */
+    @PreAuthorize("@ss.hasPermi('reliability:rel_device:query')")
+    @GetMapping(value = "/dueSoonCompos")
+    public AjaxResult listDueSoonCompos(@RequestParam("devTag") String devTag)
+    {
+        if (StringUtils.isEmpty(devTag)) {
+            return AjaxResult.error("devTag不能为空");
+        }
+
+        List<TRelCompo> compoList = tRelCompoService.selectTRelCompoByDevTag(devTag);
+        if (compoList == null || compoList.isEmpty()) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+
+        Set<String> dueSoonCompoNames = new LinkedHashSet<>();
+        for (TRelCompo compo : compoList) {
+            if (compo == null || StringUtils.isEmpty(compo.getCompoName())) {
+                continue;
+            }
+
+            // “但是没有延期”:只要该部件存在任一类型的逾期,则不进入即将到期
+            boolean anyOverdue = isOverdueByLastAndFreq(compo.getLastInspDate(), compo.getInspFreq())
+                || isOverdueByLastAndFreq(compo.getLastFixDate(), compo.getFixFreq())
+                || isOverdueByLastAndFreq(compo.getLastReplaceDate(), compo.getReplaceFreq());
+            if (anyOverdue) {
+                continue;
+            }
+
+            boolean inspDueSoon = isDueSoonByLastAndFreq(compo.getLastInspDate(), compo.getInspFreq());
+            boolean fixDueSoon = isDueSoonByLastAndFreq(compo.getLastFixDate(), compo.getFixFreq());
+            boolean replaceDueSoon = isDueSoonByLastAndFreq(compo.getLastReplaceDate(), compo.getReplaceFreq());
+
+            if (inspDueSoon || fixDueSoon || replaceDueSoon) {
+                dueSoonCompoNames.add(compo.getCompoName());
+            }
+        }
+
+        return AjaxResult.success(new ArrayList<>(dueSoonCompoNames));
+    }
+
+    private boolean isOverdueByLastAndFreq(Date lastDate, String freq)
+    {
+        // 需求约束:没有“上一次日期”,则不参与延期计算
+        if (lastDate == null) {
+            return false;
+        }
+        Date nextDate = MaintFrequencyUtils.calculateNextDate(lastDate, freq);
+        return MaintFrequencyUtils.isOverdue(nextDate);
+    }
+
+    private boolean isDueSoonByLastAndFreq(Date lastDate, String freq)
+    {
+        // 需求约束:没有“上一次日期”,则不参与计算
+        if (lastDate == null) {
+            return false;
+        }
+
+        int freqDays = MaintFrequencyUtils.parseToDays(freq);
+        if (freqDays <= 0) {
+            return false;
+        }
+
+        Date nextDate = MaintFrequencyUtils.calculateNextDate(lastDate, freq);
+        if (nextDate == null) {
+            return false;
+        }
+
+        // 不能延期:已逾期的直接排除
+        if (MaintFrequencyUtils.isOverdue(nextDate)) {
+            return false;
+        }
+
+        int remainingDays = MaintFrequencyUtils.daysFromNow(nextDate);
+
+        // 周期最后20%以内:剩余天数 <= 周期天数*20%
+        // 注意:阈值至少为1天,避免周期较短时阈值为0导致看不到“即将到期”
+        int thresholdDays = (int) Math.ceil(freqDays * 0.2d);
+        if (thresholdDays < 1) {
+            thresholdDays = 1;
+        }
+
+        return remainingDays >= 0 && remainingDays <= thresholdDays;
+    }
+
     /**
      * 新增可靠性设备清单
      */

+ 52 - 0
master/src/main/java/com/ruoyi/project/reliability/controller/TRelMaintPlanController.java

@@ -180,6 +180,13 @@ public class TRelMaintPlanController extends BaseController
             logger.info("维修计划申请" + JSON.toJSONString(tRelMaintPlan));
             logger.info("维修部件数量: " + (tRelMaintPlan.getMaintComponents() != null ? tRelMaintPlan.getMaintComponents().size() : 0));
 
+            // ===== 参数校验:勾选了需要维修的部件必须填写维修形式和负责人(防止前端绕过) =====
+            // 说明:maintComponents 为“本次选择需要维修的部件列表”(未选择的不传或为空)。
+            String maintComponentsErr = validateMaintComponents(tRelMaintPlan.getMaintComponents());
+            if (maintComponentsErr != null) {
+                return AjaxResult.error(maintComponentsErr);
+            }
+
             Long userId = getUserId();
 
             // 记录原来的计划ID(如果是计划中状态的修改提交)
@@ -333,6 +340,11 @@ public class TRelMaintPlanController extends BaseController
     @PutMapping
     public AjaxResult edit(@RequestBody TRelMaintPlan tRelMaintPlan)
     {
+        // ===== 参数校验:勾选了需要维修的部件必须填写维修形式和负责人(防止前端绕过) =====
+        String maintComponentsErr = validateMaintComponents(tRelMaintPlan != null ? tRelMaintPlan.getMaintComponents() : null);
+        if (maintComponentsErr != null) {
+            return AjaxResult.error(maintComponentsErr);
+        }
         return toAjax(tRelMaintPlanService.updateTRelMaintPlanWithSync(tRelMaintPlan));
     }
 
@@ -348,6 +360,12 @@ public class TRelMaintPlanController extends BaseController
                 return AjaxResult.error("维修部件列表不能为空");
             }
 
+            // ===== 参数校验:勾选了需要维修的部件必须填写维修形式和负责人 =====
+            String maintComponentsErr = validateMaintComponents(tRelMaintPlan.getMaintComponents());
+            if (maintComponentsErr != null) {
+                return AjaxResult.error(maintComponentsErr);
+            }
+
             tRelMaintPlanService.syncMaintRecordsOnly(tRelMaintPlan, getUserId());
             return AjaxResult.success("保存成功");
         } catch (Exception e) {
@@ -369,6 +387,12 @@ public class TRelMaintPlanController extends BaseController
             logger.info("维修计划再次提交申请" + JSON.toJSONString(tRelMaintPlan));
             logger.info("维修部件数量: " + (tRelMaintPlan.getMaintComponents() != null ? tRelMaintPlan.getMaintComponents().size() : 0));
 
+            // ===== 参数校验:勾选了需要维修的部件必须填写维修形式和负责人(防止前端绕过) =====
+            String maintComponentsErr = validateMaintComponents(tRelMaintPlan.getMaintComponents());
+            if (maintComponentsErr != null) {
+                return AjaxResult.error(maintComponentsErr);
+            }
+
             Long userId = getUserId();
 
             // 检查计划是否存在且审批状态为已通过
@@ -592,6 +616,34 @@ public class TRelMaintPlanController extends BaseController
         }
     }
 
+    /**
+     * 校验前端传入的“本次需要维修的部件列表”。
+     * 规则:只要在列表中出现的部件(即用户勾选了“需要维修”),就必须填写 maintType 和 responsible。
+     *
+     * @param maintComponents 前端传入的维修部件列表(可能为 null 或空)
+     * @return 错误信息;校验通过返回 null
+     */
+    private String validateMaintComponents(List<TRelMaintRecord> maintComponents) {
+        if (maintComponents == null || maintComponents.isEmpty()) {
+            return null;
+        }
+
+        for (TRelMaintRecord r : maintComponents) {
+            if (r == null) {
+                continue;
+            }
+            // compoName 仅用于提示信息;字段缺失不影响校验逻辑
+            String compoName = StringUtils.isNotEmpty(r.getCompoName()) ? r.getCompoName() : "-";
+            if (StringUtils.isEmpty(r.getMaintType())) {
+                return "部件【" + compoName + "】已选择需要维修,请先选择维修形式";
+            }
+            if (StringUtils.isEmpty(r.getResponsible())) {
+                return "部件【" + compoName + "】已选择需要维修,请先选择负责人";
+            }
+        }
+        return null;
+    }
+
     /**
      * 审批后处理维修记录状态
      * @param planId 计划ID

+ 230 - 72
master/src/main/java/com/ruoyi/project/reliability/service/impl/MaintPlanGeneratorServiceImpl.java

@@ -117,9 +117,9 @@ public class MaintPlanGeneratorServiceImpl implements IMaintPlanGeneratorService
         endCal.add(Calendar.MONTH, monthsAhead);
         Date endDate = endCal.getTime();
 
-        // 4. 计算每个部件的维护任务,按月份分组
-        // Key: 年月(如 "2025-06"), Value: 该月需要维护的部件任务列表
-        Map<String, List<CompoMaintTask>> monthlyTasks = new TreeMap<>();
+        // 4. 计算每个部件的维护任务(统一汇总到“单一计划”)
+        // 一个设备在窗口期内只生成一张计划;所有任务撮合到同一计划(允许个别任务超期)
+        List<CompoMaintTask> allTasks = new ArrayList<>();
 
         // 备忘录纳入规则:若某部件存在备忘录(t_rel_maint_memo),也需要强制纳入本次生成
         // 将备忘录按 compoId 分组,后续在遍历部件时追加“备忘录任务”到 monthlyTasks
@@ -137,9 +137,7 @@ public class MaintPlanGeneratorServiceImpl implements IMaintPlanGeneratorService
         for (TRelCompo compo : compoList) {
             CompoMaintTask task = calculateOptimalTask(compo, now, endDate, mergeThresholdDays);
             if (task != null) {
-                // 按月份分组
-                String monthKey = getMonthKey(task.maintDate);
-                monthlyTasks.computeIfAbsent(monthKey, k -> new ArrayList<>()).add(task);
+                allTasks.add(task);
             }
 
             if (compo != null && compo.getCompoId() != null) {
@@ -150,64 +148,71 @@ public class MaintPlanGeneratorServiceImpl implements IMaintPlanGeneratorService
                     for (TRelMaintMemo memo : compoMemoList) {
                         CompoMaintTask memoTask = buildTaskFromMemo(compo, memo, now, endDate);
                         if (memoTask != null) {
-                            String monthKey = getMonthKey(memoTask.maintDate);
-                            monthlyTasks.computeIfAbsent(monthKey, k -> new ArrayList<>()).add(memoTask);
+                            allTasks.add(memoTask);
                         }
                     }
                 }
             }
         }
 
-        if (monthlyTasks.isEmpty()) {
+        // ===== 任务撮合规则(重要):一个计划中同一个部件只保留一个任务 =====
+        // 1) 优先级:更换(3) > 维修(2) > 检查(1)
+        // 2) 若部件存在备忘录任务:
+        //    - 备注仍需标识为“从备忘录添加”(记录层面)
+        //    - 若自动计算任务为“更换(3)”而备忘录为“维修(2)”,则保留备忘录任务但将其升级为“更换(3)”,备注依然为备忘录
+        //    - 若都是维修(2),则仅保留备忘录任务(避免同部件重复任务)
+        allTasks = mergeTasksOnePerCompo(allTasks);
+
+        if (allTasks.isEmpty()) {
             logger.info("设备{}在未来{}个月内没有需要维护的部件", devTag, monthsAhead);
             return 0;
         }
 
-        // 5. 为每个月份生成一个维修计划
-        int planCount = 0;
-        for (Map.Entry<String, List<CompoMaintTask>> entry : monthlyTasks.entrySet()) {
-            String monthKey = entry.getKey();
-            List<CompoMaintTask> tasks = entry.getValue();
-
-            // 计算该月份的计划开始日期
-            Date planStartDate = getEarliestDate(tasks);
-
-            // 创建维修计划
-            TRelMaintPlan plan = new TRelMaintPlan();
-            plan.setPlant(device.getPlant());
-            plan.setDevName(device.getDevName());
-            plan.setDevTag(device.getDevTag());
-            plan.setPlanTime(planStartDate);
-            // 不设置计划结束时间
-            plan.setApprovalStatus(STATUS_PLANNED);  // 9-未开始
-            plan.setCompletionStatus(STATUS_PLANNED); // 9-计划中
-
-            // 若该月计划中包含“备忘录任务”,在计划备注中追加来源标识,便于客户识别
-            boolean hasMemoTask = false;
-            if (tasks != null && !tasks.isEmpty()) {
-                for (CompoMaintTask t : tasks) {
-                    if (t != null && t.fromMemo) {
-                        hasMemoTask = true;
-                        break;
-                    }
-                }
-            }
-            plan.setRemarks(hasMemoTask ? ("自动生成的" + monthKey + "维修计划(从备忘录添加)") : ("自动生成的" + monthKey + "维修计划"));
+        // 5. 将所有任务撮合到“一张计划”:计划日期取“最早与最晚的中位时间”,若早于今天则取今天
+        Date earliestDate = getEarliestDate(allTasks);
+        Date latestDate = getLatestDate(allTasks);
+        Date planStartDate = earliestDate;
+        if (earliestDate != null && latestDate != null) {
+            long mid = (earliestDate.getTime() + latestDate.getTime()) / 2;
+            planStartDate = new Date(mid);
+        }
+        if (planStartDate != null && planStartDate.before(new Date())) {
+            planStartDate = new Date();
+        }
 
-            maintPlanService.insertTRelMaintPlan(plan);
-            Long planId = plan.getPlanId();
+        TRelMaintPlan plan = new TRelMaintPlan();
+        plan.setPlant(device.getPlant());
+        plan.setDevName(device.getDevName());
+        plan.setDevTag(device.getDevTag());
+        plan.setPlanTime(planStartDate);
+        plan.setApprovalStatus(STATUS_PLANNED);   // 9-未开始
+        plan.setCompletionStatus(STATUS_PLANNED); // 9-计划中
 
-            // 创建维修记录
-            for (CompoMaintTask task : tasks) {
-                TRelMaintRecord record = createMaintRecord(device, task, planId);
-                maintRecordService.insertTRelMaintRecord(record);
+        boolean hasMemoTask = false;
+        for (CompoMaintTask t : allTasks) {
+            if (t != null && t.fromMemo) {
+                hasMemoTask = true;
+                break;
             }
+        }
+        plan.setRemarks(hasMemoTask ? "自动生成的维修计划(从备忘录添加)" : "自动生成的维修计划");
+
+        maintPlanService.insertTRelMaintPlan(plan);
+        Long planId = plan.getPlanId();
 
-            planCount++;
-            logger.info("为设备{}生成{}维修计划,包含{}个部件", devTag, monthKey, tasks.size());
+        // 将所有任务的计划日期统一撮合到 planStartDate(可能导致部分任务相对原日期略有超期/提前)
+        for (CompoMaintTask task : allTasks) {
+            task.maintDate = planStartDate;
         }
 
-        return planCount;
+        // 创建维修记录
+        for (CompoMaintTask task : allTasks) {
+            TRelMaintRecord record = createMaintRecord(device, task, planId);
+            maintRecordService.insertTRelMaintRecord(record);
+        }
+
+        logger.info("为设备{}生成1张维修计划,包含{}个任务", devTag, allTasks.size());
+        return 1;
     }
 
     private int cleanupOldPlannedPlans(String devTag) {
@@ -258,16 +263,11 @@ public class MaintPlanGeneratorServiceImpl implements IMaintPlanGeneratorService
     }
 
     private CompoMaintTask buildTaskFromMemo(TRelCompo compo, TRelMaintMemo memo, Date now, Date endDate) {
-        // 将备忘录转成可参与生成的任务:使用 memoTime 作为任务日期(为空则使用当前日期
+        // 将备忘录转成可参与生成的任务:备忘时间不影响决策,仅用于生成记录,日期统一使用当前计划日期(后续撮合
         if (compo == null || memo == null) {
             return null;
         }
 
-        Date memoTime = memo.getMemoTime();
-        if (memoTime != null && memoTime.after(endDate)) {
-            return null;
-        }
-
         String maintType = memo.getMaintType();
         if (maintType == null || maintType.trim().isEmpty()) {
             // 兜底:防止客户未填维修类型导致生成记录 maintType 为空
@@ -286,16 +286,10 @@ public class MaintPlanGeneratorServiceImpl implements IMaintPlanGeneratorService
             }
         }
 
-        Date taskDate = memoTime != null ? memoTime : now;
-        // 若备忘录时间早于当前日期,为避免计划开始时间落在过去,这里按当前日期落计划
-        if (taskDate.before(now)) {
-            taskDate = now;
-        }
-
         CompoMaintTask task = new CompoMaintTask();
         task.compo = compo;
         task.maintType = maintType;
-        task.maintDate = taskDate;
+        task.maintDate = now; // 实际会在撮合时统一覆盖为计划日期
         task.maintContent = maintContent;
         task.responsible = responsible;
         task.fromMemo = true;
@@ -458,17 +452,6 @@ public class MaintPlanGeneratorServiceImpl implements IMaintPlanGeneratorService
         return !date.after(endDate);
     }
 
-    /**
-     * 获取月份Key,格式:yyyy-MM
-     */
-    private String getMonthKey(Date date) {
-        Calendar cal = Calendar.getInstance();
-        cal.setTime(date);
-        int year = cal.get(Calendar.YEAR);
-        int month = cal.get(Calendar.MONTH) + 1;
-        return String.format("%04d-%02d", year, month);
-    }
-
     /**
      * 获取任务列表中最早的日期
      */
@@ -529,6 +512,181 @@ public class MaintPlanGeneratorServiceImpl implements IMaintPlanGeneratorService
         return record;
     }
 
+    /**
+     * 将任务列表合并为“每个部件仅保留一个任务”。
+     *
+     * 说明:
+     * - 当前实现的自动生成会为同一部件同时加入“自动计算任务”和“备忘录任务”,且同部件可能有多条备忘录。
+     * - 该方法用于落库前做最终撮合,避免同一计划中同一部件出现重复任务。
+     */
+    private List<CompoMaintTask> mergeTasksOnePerCompo(List<CompoMaintTask> allTasks) {
+        if (allTasks == null || allTasks.isEmpty()) {
+            return allTasks;
+        }
+
+        // 使用 LinkedHashMap 保持相对稳定的输出顺序
+        Map<String, List<CompoMaintTask>> taskMap = new LinkedHashMap<>();
+        for (CompoMaintTask t : allTasks) {
+            if (t == null || t.compo == null || t.compo.getCompoId() == null) {
+                continue;
+            }
+            String compoId = String.valueOf(t.compo.getCompoId());
+            taskMap.computeIfAbsent(compoId, k -> new ArrayList<>()).add(t);
+        }
+
+        List<CompoMaintTask> merged = new ArrayList<>();
+        int removedCount = 0;
+        int upgradedCount = 0;
+
+        for (Map.Entry<String, List<CompoMaintTask>> entry : taskMap.entrySet()) {
+            List<CompoMaintTask> list = entry.getValue();
+            if (list == null || list.isEmpty()) {
+                continue;
+            }
+            if (list.size() == 1) {
+                merged.add(list.get(0));
+                continue;
+            }
+
+            // 优先挑选“备忘录任务”作为基准(确保备注为备忘录)
+            CompoMaintTask bestMemoTask = null;
+            CompoMaintTask bestAutoTask = null;
+            for (CompoMaintTask t : list) {
+                if (t == null) {
+                    continue;
+                }
+                if (t.fromMemo) {
+                    bestMemoTask = pickBetterTask(bestMemoTask, t);
+                } else {
+                    bestAutoTask = pickBetterTask(bestAutoTask, t);
+                }
+            }
+
+            CompoMaintTask kept;
+            if (bestMemoTask != null) {
+                kept = bestMemoTask;
+                // 若自动计算任务优先级更高(通常是更换),则将备忘录任务升级为更高类型,但保留“fromMemo”标识
+                if (bestAutoTask != null && taskPriority(bestAutoTask.maintType) > taskPriority(bestMemoTask.maintType)) {
+                    String newType = bestAutoTask.maintType;
+                    upgradeTaskTypeKeepMemoRemark(kept, newType);
+                    upgradedCount++;
+                }
+            } else {
+                // 没有备忘录任务:按优先级保留一个
+                kept = bestAutoTask;
+            }
+
+            if (kept != null) {
+                merged.add(kept);
+                removedCount += (list.size() - 1);
+            }
+        }
+
+        logger.info("自动生成任务撮合完成:输入任务{}个,输出任务{}个,去重移除{}个,升级任务类型{}次", allTasks.size(), merged.size(), removedCount, upgradedCount);
+        return merged;
+    }
+
+    /**
+     * 按优先级挑选更“重要”的任务。
+     * 优先级:更换(3) > 维修(2) > 检查(1)
+     * 若优先级相同,则选择日期更早的(更紧急)。
+     */
+    private CompoMaintTask pickBetterTask(CompoMaintTask a, CompoMaintTask b) {
+        if (a == null) {
+            return b;
+        }
+        if (b == null) {
+            return a;
+        }
+
+        int pa = taskPriority(a.maintType);
+        int pb = taskPriority(b.maintType);
+        if (pb > pa) {
+            return b;
+        }
+        if (pb < pa) {
+            return a;
+        }
+
+        // 同优先级:日期更早者更紧急
+        if (a.maintDate == null) {
+            return b;
+        }
+        if (b.maintDate == null) {
+            return a;
+        }
+        return b.maintDate.before(a.maintDate) ? b : a;
+    }
+
+    private int taskPriority(String maintType) {
+        // 更换(3) > 维修(2) > 检查(1)
+        if ("3".equals(maintType)) {
+            return 3;
+        }
+        if ("2".equals(maintType)) {
+            return 2;
+        }
+        if ("1".equals(maintType)) {
+            return 1;
+        }
+        return 0;
+    }
+
+    /**
+     * 将备忘录任务“升级”为更高优先级的维修类型。
+     * 注意:
+     * - 仍保留 fromMemo=true(用于落库时写备注“从备忘录添加”)
+     * - 同步更新 content/responsible,以匹配升级后的类型(尽量使用部件默认内容;缺失时保持原内容)
+     */
+    private void upgradeTaskTypeKeepMemoRemark(CompoMaintTask task, String newType) {
+        if (task == null || task.compo == null) {
+            return;
+        }
+        if (newType == null || newType.trim().isEmpty()) {
+            return;
+        }
+
+        task.maintType = newType;
+        if ("3".equals(newType)) {
+            // 备注来源仍为备忘录:尽量保留备忘录填写的内容/负责人,只有为空时才回填部件默认值
+            if (task.maintContent == null || task.maintContent.trim().isEmpty()) {
+                if (task.compo.getReplaceContent() != null && !task.compo.getReplaceContent().trim().isEmpty()) {
+                    task.maintContent = task.compo.getReplaceContent();
+                }
+            }
+            if (task.responsible == null || task.responsible.trim().isEmpty()) {
+                if (task.compo.getFixer() != null && !task.compo.getFixer().trim().isEmpty()) {
+                    task.responsible = task.compo.getFixer();
+                }
+            }
+        } else if ("2".equals(newType)) {
+            if (task.maintContent == null || task.maintContent.trim().isEmpty()) {
+                if (task.compo.getFixContent() != null && !task.compo.getFixContent().trim().isEmpty()) {
+                    task.maintContent = task.compo.getFixContent();
+                }
+            }
+            if (task.responsible == null || task.responsible.trim().isEmpty()) {
+                if (task.compo.getFixer() != null && !task.compo.getFixer().trim().isEmpty()) {
+                    task.responsible = task.compo.getFixer();
+                }
+            }
+        } else if ("1".equals(newType)) {
+            if (task.maintContent == null || task.maintContent.trim().isEmpty()) {
+                if (task.compo.getInspContent() != null && !task.compo.getInspContent().trim().isEmpty()) {
+                    task.maintContent = task.compo.getInspContent();
+                }
+            }
+            if (task.responsible == null || task.responsible.trim().isEmpty()) {
+                if (task.compo.getInspector() != null && !task.compo.getInspector().trim().isEmpty()) {
+                    task.responsible = task.compo.getInspector();
+                }
+            }
+        }
+
+        // 强制保持备忘录标识,确保备注写入
+        task.fromMemo = true;
+    }
+
     /**
      * 部件维护任务内部类
      */

+ 16 - 0
ui/src/api/reliability/rel_device.js

@@ -17,6 +17,22 @@ export function getRel_device(devId) {
   })
 }
 
+export function listOverdueCompos(devTag) {
+  return request({
+    url: '/reliability/rel_device/overdueCompos',
+    method: 'get',
+    params: { devTag }
+  })
+}
+
+export function listDueSoonCompos(devTag) {
+  return request({
+    url: '/reliability/rel_device/dueSoonCompos',
+    method: 'get',
+    params: { devTag }
+  })
+}
+
 // 新增可靠性设备清单
 export function addRel_device(data) {
   return request({

+ 2 - 2
ui/src/assets/visio/12M.svg

@@ -168,7 +168,7 @@
 			<rect x="0" y="261.514" width="123.72" height="338.944" class="st1"/>
 		</g>
 		<g id="shape1001-3" v:mID="1001" v:groupContext="shape" transform="translate(426.902,-537.191)">
-			<title>正方形.6</title>
+			<title>风道挡板.1001</title>
 			<v:userDefs>
 				<v:ud v:nameU="visVersion" v:val="VT0(15):26"/>
 			</v:userDefs>
@@ -2034,7 +2034,7 @@
 			<path d="M1.98 593.37 L12.19 593.37" class="st22"/>
 		</g>
 		<g id="shape1230-484" v:mID="1230" v:groupContext="shape" transform="translate(427.445,-537.358)">
-			<title>工作表.1230</title>
+			<title>风道挡板</title>
 			<path d="M41.93 600.46 L41.93 558.53 L0 558.53 L0 600.46 L41.93 600.46 ZM20.97 579.49 L6.29 583.69 A4.62798 2.94627 90
 						 1 1 6.29 575.3 L20.97 579.49 L35.64 575.3 A4.62798 2.94627 90 1 1 35.64 583.69 L20.97 579.49 Z"
 					class="st23"/>

+ 12 - 12
ui/src/assets/visio/224U.svg

@@ -404,9 +404,9 @@
 			<title>开工线</title>
 			<path d="M0 500.19 L-37.48 500.19 L-37.48 531.02" class="st8"/>
 		</g>
-		<g id="shape1045-79" v:mID="1045" v:groupContext="shape" v:layerMember="1" transform="translate(470.473,-245.078)">
+		<g id="shape1045-79" v:mID="1045" v:groupContext="shape" v:layerMember="1" transform="translate(470.473,-287.031)">
 			<title>工艺水管线</title>
-			<path d="M0 500.19 L0 436.45 L42.57 436.45" class="st8"/>
+			<path d="M0 500.19 L0 478.4 L42.57 478.4" class="st8"/>
 		</g>
 		<g id="shape1046-82" v:mID="1046" v:groupContext="shape" v:layerMember="1" transform="translate(470.473,-245.078)">
 			<title>急冷油管线</title>
@@ -550,7 +550,7 @@
 			<path d="M0 490.41 L0 496.93 L13.04 490.41 L13.04 496.93 L0 490.41 Z" class="st14"/>
 			<text x="-5.48" y="507.81" class="st13" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>原料扫线</text>		</g>
 		<g id="shape1055-105" v:mID="1055" v:groupContext="shape" v:layerMember="1" transform="translate(110.031,-396.622)">
-			<title>动态连接线.1019</title>
+			<title>原料扫线.1055</title>
 			<path d="M-2.27 493.1 L-11.91 493.1" class="st8"/>
 		</g>
 		<g id="shape1056-108" v:mID="1056" v:groupContext="shape" v:layerMember="1" transform="translate(110.031,-430.18)">
@@ -851,7 +851,7 @@
 					class="st8"/>
 		</g>
 		<g id="shape1085-179" v:mID="1085" v:groupContext="shape" v:layerMember="1" transform="translate(492.263,-371.952)">
-			<title>动态连接线.1051</title>
+			<title>减温水管线.1085</title>
 			<path d="M-7.02 500.19 L-7.15 523.49" class="st8"/>
 		</g>
 		<g id="shape1086-182" v:mID="1086" v:groupContext="shape" v:layerMember="3" transform="translate(513.045,-302.968)">
@@ -923,7 +923,7 @@
 			<path d="M3.51 489.95 L8.19 489.95 A2.60226 2.60226 -180 0 0 3.51 489.95" class="st11"/>
 			<text x="-3.15" y="507.64" class="st13" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>急冷油</text>		</g>
 		<g id="shape1088-194" v:mID="1088" v:groupContext="shape" v:layerMember="1" transform="translate(538.205,-279.944)">
-			<title>动态连接线.1054</title>
+			<title>急冷油管线.1088</title>
 			<path d="M-0.71 493.1 L-13.46 493.1" class="st9"/>
 		</g>
 		<g id="shape1089-197" v:mID="1089" v:groupContext="shape" v:layerMember="1" transform="translate(523.606,-315.64)">
@@ -1192,7 +1192,7 @@
 						 495.84 A2.60787 2.60787 0 1 1 1.3 495.84" class="st25"/>
 		</g>
 		<g id="shape1114-255" v:mID="1114" v:groupContext="shape" v:layerMember="1" transform="translate(199.039,-371.023)">
-			<title>动态连接线.1080</title>
+			<title>DS总管.1114</title>
 			<path d="M0 493.12 L14.28 493.08" class="st8"/>
 		</g>
 		<g id="shape1115-258" v:mID="1115" v:groupContext="shape" v:layerMember="4"
@@ -1227,7 +1227,7 @@
 						 495.84 A2.60787 2.60787 0 1 1 1.3 495.84" class="st25"/>
 		</g>
 		<g id="shape1116-262" v:mID="1116" v:groupContext="shape" v:layerMember="1" transform="translate(492.231,-382.038)">
-			<title>动态连接线.1084</title>
+			<title>减温水管线.1116</title>
 			<path d="M-6.99 497.23 L-7.19 488.97" class="st8"/>
 		</g>
 		<g id="shape1117-265" v:mID="1117" v:groupContext="shape" v:layerMember="4" transform="translate(536.646,-303.944)">
@@ -1881,7 +1881,7 @@
 			<path d="M0 500.19 L0 489.56 L27.64 489.56 L27.64 530.93 L115.16 530.93" class="st8"/>
 		</g>
 		<g id="shape1182-445" v:mID="1182" v:groupContext="shape" v:layerMember="1" transform="translate(361.578,-273.298)">
-			<title>动态连接线.1182</title>
+			<title>SS一段过热</title>
 			<path d="M7.09 500.19 L28.47 500.19 L28.47 530.13 L7.09 530.13" class="st8"/>
 		</g>
 		<g id="shape1183-448" v:mID="1183" v:groupContext="shape" v:layerMember="1" transform="translate(294.069,-236.27)">
@@ -1956,7 +1956,7 @@
 			<path d="M0 497.14 L15.41 497.14 L15.41 489.06" class="st8"/>
 		</g>
 		<g id="shape1194-481" v:mID="1194" v:groupContext="shape" v:layerMember="1" transform="translate(243.562,-411.323)">
-			<title>动态连接线.1194</title>
+			<title>烧焦空气管线.1194</title>
 			<path d="M-7.04 497.77 L-7.13 488.43" class="st8"/>
 		</g>
 		<g id="shape1195-484" v:mID="1195" v:groupContext="shape" v:layerMember="1" transform="translate(120.803,-416.007)">
@@ -2199,7 +2199,7 @@
 			<path d="M0 500.19 L0 556.54 L56.69 556.54 L56.69 589.81 L75.4 589.81" class="st8"/>
 		</g>
 		<g id="shape1212-549" v:mID="1212" v:groupContext="shape" v:layerMember="1" transform="translate(252.971,-295.419)">
-			<title>动态连接线.1212</title>
+			<title>减温蒸汽管线.1212</title>
 			<path d="M0 507.11 L147.59 507.43" class="st8"/>
 		</g>
 		<g id="shape1214-552" v:mID="1214" v:groupContext="shape" transform="translate(313.918,-445.174)">
@@ -2211,11 +2211,11 @@
 		<g id="group1217-554" transform="translate(315.619,-458.428)" v:mID="1217" v:groupContext="group">
 			<title>风机</title>
 			<g id="shape1215-555" v:mID="1215" v:groupContext="shape">
-				<title>工作表.1215</title>
+				<title>风机</title>
 				<path d="M15.31 496.43 L3.4 493.03 A3.75434 2.39009 -90 1 0 3.4 499.83 L15.31 496.43 Z" class="st7"/>
 			</g>
 			<g id="shape1216-557" v:mID="1216" v:groupContext="shape" transform="translate(15.3071,-5.79803E-12)">
-				<title>工作表.1216</title>
+				<title>风机.1216</title>
 				<path d="M0 496.43 L11.91 499.83 A3.75434 2.39009 -90 1 0 11.91 493.03 L0 496.43 Z" class="st7"/>
 			</g>
 		</g>

+ 11 - 11
ui/src/assets/visio/80U.svg

@@ -179,7 +179,7 @@
 			<rect x="0" y="224.972" width="100.365" height="274.961" class="st1"/>
 		</g>
 		<g id="shape6-3" v:mID="6" v:groupContext="shape" transform="translate(318.254,-444.785)">
-			<title>正方形.6</title>
+			<title>烟道挡板.6</title>
 			<v:userDefs>
 				<v:ud v:nameU="visVersion" v:val="VT0(15):26"/>
 			</v:userDefs>
@@ -516,17 +516,17 @@
 			<path d="M0 499.93 L0 655.46 L-181.5 655.46" class="st7"/>
 		</g>
 		<g id="shape1005-114" v:mID="1005" v:groupContext="shape" v:layerMember="2" transform="translate(275.145,-80.1464)">
-			<title>动态连接线.1005</title>
+			<title>烧焦气管线.1005</title>
 			<path d="M0 499.93 L-0 537.08 L16.73 537.08 A2.3622 2.3622 0 1 1 21.46 537.08 L63.03 537.08" class="st7"/>
 		</g>
 		<g id="shape1006-117" v:mID="1006" v:groupContext="shape" v:layerMember="2" transform="translate(394.125,-80.1464)">
-			<title>动态连接线.1006</title>
+			<title>烧焦气管线.1006</title>
 			<path d="M0 499.93 L0 537.08 L-53.11 537.08" class="st7"/>
 		</g>
-		<g id="shape1007-120" v:mID="1007" v:groupContext="shape" v:layerMember="2" transform="translate(555.028,-220.398)">
+		<g id="shape1007-120" v:mID="1007" v:groupContext="shape" v:layerMember="2" transform="translate(555.028,-220.54)">
 			<title>裂解气总管.1007</title>
 			<v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
-			<path d="M0 492.89 L-35.43 492.8" class="st7"/>
+			<path d="M0 493.04 L-66.17 492.66" class="st7"/>
 		</g>
 		<g id="shape1008-123" v:mID="1008" v:groupContext="shape" v:layerMember="2" transform="translate(557.073,-258.431)">
 			<title>开工线.1008</title>
@@ -534,7 +534,7 @@
 		</g>
 		<g id="shape1009-126" v:mID="1009" v:groupContext="shape" v:layerMember="2" transform="translate(475.25,-286.777)">
 			<title>工艺水管线.1009</title>
-			<path d="M0 499.93 L0 478.15 L42.57 478.15" class="st12"/>
+			<path d="M0 499.93 L10.63 499.93 L10.63 478.15 L42.57 478.15" class="st12"/>
 		</g>
 		<g id="shape1010-129" v:mID="1010" v:groupContext="shape" v:layerMember="2" transform="translate(475.25,-244.824)">
 			<title>急冷油管线.1010</title>
@@ -1161,9 +1161,9 @@
 			<rect x="0" y="491.928" width="28.3465" height="8.00463" class="st18"/>
 			<text x="5.17" y="494.33" class="st16" v:langID="2052"><v:paragraph v:horizAlign="1"/><v:tabList/>电动阀<v:newlineChar/><tspan
 						x="8.09" dy="1.233em" class="st25">1303</tspan></text>		</g>
-		<g id="shape1066-293" v:mID="1066" v:groupContext="shape" v:layerMember="2" transform="translate(488.857,-227.817)">
-			<title>动态连接线.1066</title>
-			<path d="M0 499.93 L30.82 499.93 L30.82 518.73" class="st7"/>
+		<g id="shape1066-293" v:mID="1066" v:groupContext="shape" v:layerMember="2" transform="translate(526.802,-227.639)">
+			<title>烧焦气管线.1066</title>
+			<path d="M-7.05 499.93 L-7.05 505.25 L-7.13 505.25 L-7.13 518.55" class="st7"/>
 		</g>
 		<g id="shape1067-296" v:mID="1067" v:groupContext="shape" v:layerMember="3" transform="translate(557.073,-253.187)">
 			<title>开工大阀</title>
@@ -2000,7 +2000,7 @@
 			<path d="M0 496.88 L15.41 496.88 L15.41 488.81" class="st12"/>
 		</g>
 		<g id="shape1124-487" v:mID="1124" v:groupContext="shape" v:layerMember="2" transform="translate(248.339,-411.036)">
-			<title>动态连接线.1124</title>
+			<title>烧焦空气管线.1124</title>
 			<path d="M-7.04 497.48 L-7.13 488.21" class="st12"/>
 		</g>
 		<g id="shape1125-490" v:mID="1125" v:groupContext="shape" transform="translate(-240.672,107.118) rotate(-90)">
@@ -2165,7 +2165,7 @@
 			</g>
 		</g>
 		<g id="shape1139-536" v:mID="1139" v:groupContext="shape" transform="translate(318.695,-444.921)">
-			<title>工作表.1139</title>
+			<title>烟道挡板</title>
 			<path d="M34.02 499.93 L34.02 465.92 L0 465.92 L0 499.93 L34.02 499.93 ZM17.01 482.92 L5.1 486.33 A3.75434 2.39009 90
 						 1 1 5.1 479.52 L17.01 482.92 L28.91 479.52 A3.75434 2.39009 90 1 1 28.91 486.33 L17.01 482.92 Z"
 					class="st11"/>

+ 11 - 7
ui/src/views/eoeg/eoegStaffmgr/index.vue

@@ -753,8 +753,15 @@
         enAbilityOptions: [],
         // 特殊职能字典
         specialDutyOptions: [],
-        //负责区域字典
-        regionOptions: [],
+        // 负责区域下拉选项(EOEG 固定值,不走字典)
+        regionOptions: [
+          { dictLabel: 'EO', dictValue: 'EO' },
+          { dictLabel: 'EG', dictValue: 'EG' },
+          { dictLabel: 'NIS', dictValue: 'NIS' },
+          { dictLabel: 'PEO', dictValue: 'PEO' },
+          { dictLabel: 'RTO', dictValue: 'RTO' },
+          { dictLabel: '无', dictValue: '无' }
+        ],
         //照片url
         photoUrl: '',
         //新增不可上传照片
@@ -932,7 +939,7 @@
       this.getDicts("TEAM_DIVIDE").then(response => {
         this.teamOptions = response.data;
       });
-      this.getDicts("ACTUALPOST").then(response => {
+      this.getDicts("EOEG_ACTUALPOST").then(response => {
         this.actualpostOptions = response.data;
       });
       this.getDicts("EDUCATION").then(response => {
@@ -941,12 +948,9 @@
       this.getDicts("ENGLISHABILITY").then(response => {
         this.enAbilityOptions = response.data;
       });
-      this.getDicts("SPECIAL_DUTY").then(response => {
+      this.getDicts("EOEG_SPECIAL_DUTY").then(response => {
         this.specialDutyOptions = response.data;
       });
-      this.getDicts("region_type").then(response => {
-        this.regionOptions = response.data;
-      });
     },
     methods: {
       /** 查询人员管理列表 */

+ 104 - 47
ui/src/views/plant/EOEGorganization/branch.vue

@@ -4,10 +4,10 @@
         'liequal': item.post.trim()==='安全专员'},{
         'litop': item.post.trim()==='首席经理'
         }]">
-        <div class="branch-box" @click.prevent="clickHandle(item)" v-if="item.post.trim() !== 'EHS督导'">
+        <div class="branch-box" :class="{ female: Number(item.sex) === 1 }" @click.prevent="clickHandle(item)" v-if="item.post.trim() !== 'EHS督导'">
           <div class="branch-title">{{formatPost(item.post)}}</div>
           <img class="branch-pic" :src=item.img>
-          <div class="branch-name">{{item.label}}</div>
+          <div class="branch-name">{{formatName(item.label)}}</div>
           <div class="bz-box">
             <i v-if="item.bz1" class="iconfont icon-star"></i>
             <i v-if="item.bz2" class="iconfont icon-love"></i>
@@ -25,10 +25,10 @@
           </div>
         </div>
         <!-- 平级 -->
-        <div class="branch-box" :class="item.post.trim() === '安全专员' ? 'equal':''" @click.prevent="clickHandle(item)" v-if="item.post.trim() === '安全专员'">
+        <div class="branch-box" :class="[{ equal: item.post.trim() === '安全专员' }, { female: Number(item.sex) === 1 }]" @click.prevent="clickHandle(item)" v-if="item.post.trim() === '安全专员'">
           <div class="branch-title">{{formatPost(item.post)}}</div>
           <img class="branch-pic" :src=item.img>
-          <div class="branch-name">{{item.label}}</div>
+          <div class="branch-name">{{formatName(item.label)}}</div>
           <div class="bz-box">
             <i v-if="item.bz1" class="iconfont icon-star"></i>
             <i v-if="item.bz2" class="iconfont icon-love"></i>
@@ -48,10 +48,10 @@
           <ul class="level" v-for="(secretary, se) in item.secretary" :key="se">
             <template v-for="(child, cIndex) in secretary">
               <li v-if="child.children" :class="{'odd': ((secretary.length%2) === 1), 'safe-officer': child.post.trim() === '安全专员'}" :key="'child-' + cIndex">
-                <div class="branch-box" @click.prevent="clickHandle(child)">
+                <div class="branch-box" :class="{ female: Number(child.sex) === 1 }" @click.prevent="clickHandle(child)">
                   <div class="branch-title">{{formatPost(child.post)}}</div>
                   <img class="branch-pic" :src=child.img>
-                  <div class="branch-name">{{child.label}}</div>
+                  <div class="branch-name">{{formatName(child.label)}}</div>
                   <div class="bz-box">
                     <i v-if="child.bz1" class="iconfont icon-star"></i>
                     <i v-if="child.bz2" class="iconfont icon-love"></i>
@@ -68,11 +68,11 @@
                   </div>
                 </div>
                 <ul :style="{'width': ulWidth + 'px'}">
-                  <li v-for="(schild, chIndex) in child.children" :key="chIndex">
-                    <div class="branch-box" @click.prevent="clickHandle(schild)">
+                  <li v-for="(schild, chIndex) in child.children" :key="chIndex" :class="{'safe-officer': (schild.post && schild.post.trim() === '安全专员')}">
+                    <div class="branch-box" :class="{ female: Number(schild.sex) === 1 }" @click.prevent="clickHandle(schild)">
                       <div class="branch-title">{{formatPost(schild.post)}}</div>
                       <img class="branch-pic" :src=schild.img>
-                      <div class="branch-name">{{schild.label}}</div>
+                      <div class="branch-name">{{formatName(schild.label)}}</div>
                       <div class="bz-box">
                         <i v-if="schild.bz1" class="iconfont icon-star"></i>
                         <i v-if="schild.bz2" class="iconfont icon-love"></i>
@@ -93,10 +93,10 @@
                 </ul>
               </li>
               <li v-else :key="'no-child-' + cIndex" :class="{'odd': ((secretary.length%2) === 1&&!child.children), 'safe-officer': child.post.trim() === '安全专员'}">
-                <div class="branch-box" @click.prevent="clickHandle(child)">
+                <div class="branch-box" :class="{ female: Number(child.sex) === 1 }" @click.prevent="clickHandle(child)">
                   <div class="branch-title">{{formatPost(child.post)}}</div>
                   <img class="branch-pic" :src=child.img>
-                  <div class="branch-name">{{child.label}}</div>
+                  <div class="branch-name">{{formatName(child.label)}}</div>
                   <div class="bz-box">
                     <i v-if="child.bz1" class="iconfont icon-star"></i>
                     <i v-if="child.bz2" class="iconfont icon-love"></i>
@@ -149,8 +149,8 @@ export default {
             it.forEach((secretary, ssIndex) => {
               if (secretary.children) {
                 console.log(secretary.children.length)
-                if (width < (secretary.children.length * 194)) {
-                  width = (secretary.children.length * 194)
+                if (width < (secretary.children.length * 360)) {
+                  width = (secretary.children.length * 360)
                 }
               }
             })
@@ -164,6 +164,16 @@ export default {
     clickHandle (obj) {
       this.bus.$emit('info', obj)
     },
+    /**
+     * 仅展示中文名:如果字符串里包含中文,则只取中文部分;否则原样返回
+     * 例:"Gu Lifang 顾丽芳" -> "顾丽芳"
+     */
+    formatName(name) {
+      if (!name) return ''
+      const str = String(name)
+      const chinese = str.match(/[\u4e00-\u9fa5]+/g)
+      return (chinese && chinese.length) ? chinese.join('') : str.trim()
+    },
     isHaveChild(arr){
       for (let i = 0; i < arr.length; i++) {
         if (arr[i].children) {
@@ -202,17 +212,25 @@ export default {
     }
   }
   .branch-box{
-    width: 172px;
-    min-height: 180px;
-    border: 1px solid #CCCCCC;
+    width: 320px;
+    min-height: 140px;
+    border: 2px solid #008fd3;
+    background-color: #fff;
     border-radius: 4px;
-    text-align: center;
+    text-align: left;
     color: #666;
     margin-top: 20px;
     margin-left: 10px;
     margin-right: 10px;
-    padding-bottom: 8px;
+    padding-bottom: 0;
     position: relative;
+    display: grid;
+    grid-template-columns: 100px 1fr 90px;
+    grid-template-rows: 54px 1fr;
+    overflow: hidden;
+    &.female{
+      border-color: #ff5a86;
+    }
     &.equal{
       position: absolute;
       left: 50%;
@@ -229,27 +247,40 @@ export default {
     }
   }
   .branch-box .branch-title{
-    background-color: #2E6491;
+    background-color: #008fd3;
     color: #fff;
-    height: 44px;
-    line-height: 44px;
+    height: 54px;
+    line-height: 54px;
     text-align: center;
     font-weight: bold;
-    font-size: 14px;
+    font-size: 26px;
+    letter-spacing: 6px;
+    padding-left: 0;
+    grid-column: 1 / -1;
+    grid-row: 1;
+  }
+  .branch-box.female .branch-title{
+    background-color: #ff5a86;
   }
   .branch-box .branch-pic{
-    width: 95px;
-    height: 95px;
-    margin: 0 auto;
-    border: 1px solid #2E6491;
-    margin-top: 10px;
+    width: 86px;
+    height: 86px;
+    border: none;
+    margin: 10px 0 10px 10px;
+    object-fit: cover;
+    grid-column: 1;
+    grid-row: 2;
   }
   .branch-box .branch-name{
-    font-size: 14px;
-    color: #2E6491;
-    text-align: center;
+    font-size: 22px;
+    color: #000;
+    text-align: left;
     font-weight: bold;
-    padding: 4px 10px;
+    padding: 10px;
+    grid-column: 2;
+    grid-row: 2;
+    align-self: center;
+    line-height: 1.2;
   }
 
   ul{
@@ -260,6 +291,8 @@ export default {
     li{
       position: relative;
       display: flex;
+      flex: 0 0 auto;
+      flex-shrink: 0;
       flex-direction: column;
       align-items: center;
       padding-top: 20px;
@@ -271,8 +304,8 @@ export default {
   }
   // 倒班班长这一行增加卡牌的margin
   ul.shift-leader-row li .branch-box {
-    margin-left: 160px;
-    margin-right: 160px;
+    margin-left: 240px;
+    margin-right: 240px;
   }
   ul li:before{
     content: '';
@@ -300,9 +333,16 @@ export default {
     content: none;
   }
   // 安全专员使用虚线
+  // 单独把安全专员“视觉上”往右移:用 left 位移不改变 flex 布局占位,避免同排其它卡牌跟着重新居中
+  // 同时在 :before 上补齐延伸,避免连接线出现断档
+  ul li.safe-officer{
+    left: 300px;
+  }
   ul li.safe-officer:not(:first-child):before{
     border-top: 2px dashed #2E6491;
     border-right: 2px dashed #2E6491;
+    width: calc(50% + 300px);
+    left: calc(-300px - 1px);
   }
   ul li.safe-officer:last-child:after{
     content: none;
@@ -327,9 +367,11 @@ export default {
     content: none;
   }
   ul.double{
-    width: 390px;
-    flex-wrap: wrap;
-    justify-content: space-between;
+    width: 760px;
+    display: grid;
+    grid-template-columns: 320px 320px;
+    column-gap: 120px;
+    justify-content: center;
   }
   ul.double >li >.branch-box{
     margin-left: 0;
@@ -350,20 +392,21 @@ export default {
     padding-top: 0;
   }
   ul.double>li:after,ul.double> li:before{
-    width: 27px;
+    width: 60px;
     height: 2px;
-    top: 60%;
+    top: 50%;
+    transform: translateY(-50%);
     background-color: #2E6491;
     border: none;
     border-radius: 1px;
     z-index: 2;
   }
   ul.double> li:nth-child(2n+1):after{
-    right: -25px;
+    right: -60px;
     left: auto;
   }
   ul.double >li:nth-child(2n):before{
-    left: -25px;
+    left: -60px;
   }
   ul.double >li:nth-child(2n+1):before,ul.double >li:nth-child(2n):after{content: none;}
   ul.double >li:nth-child(2n+1):last-child:after{
@@ -371,7 +414,8 @@ export default {
   }
   .iconfont{
     color: #ff0000;
-    width: 16px;
+    width: 28px;
+    font-size: 28px;
     display: inline-block;
     text-align: center;
   }
@@ -391,19 +435,32 @@ export default {
     color: #2E6491 !important; /* 蓝色:安全代表 */
   }
   .bz-box{
-    width: 18px;
-    right: 10px;
-    top: 50px;
-    position: absolute;
+    width: 70px;
+    position: static;
+    grid-column: 3;
+    grid-row: 2;
+    justify-self: center;
+    align-self: center;
+    margin-right: 0;
+    display: grid;
+    grid-template-columns: repeat(2, 28px);
+    grid-auto-rows: 28px;
+    justify-content: center;
+    align-content: center;
+    justify-items: center;
+    align-items: center;
+    gap: 6px;
+    max-height: calc(28px * 2 + 6px);
+    overflow: hidden;
   }
   .bz-box i{
-    float: right;
+    float: none;
   }
   ul.level:after{
     content: '';
     position: absolute;
     width: 2px;
-    height: 100%;
+    height: calc(100% - 4px);
     background-color: #2E6491;
     left: 50%;
     transform: translateX(-50%);

+ 19 - 7
ui/src/views/plant/EOEGorganization/index.vue

@@ -113,7 +113,7 @@
 
 <script>
 import Branch from './branch'
-import {listOgzStaffmgr} from "@/api/plant/staffmgr";
+import {listOgzStaffmgr} from "@/api/eoeg/eoegStaffmgr";
 import {getToken} from "@/utils/auth";
 import {allFileList} from "@/api/common/commonfile";
 import {getUserByUserName} from "@/api/system/user";
@@ -250,6 +250,8 @@ export default {
                   id: this.staffmgrList[i].id,
                   pId: this.staffmgrList[i].pId,
                   label: this.staffmgrList[i].name,
+                  // 性别:用于前端卡牌配色(2=女)
+                  sex: this.staffmgrList[i].sex,
                   post: post,
                   secretary: [[], [], [], []],
                   // img: 'http://47.114.101.16' + process.env.VUE_APP_BASE_API + this.staffmgrList[i].photo,
@@ -301,6 +303,8 @@ export default {
                   id: this.staffmgrList[i].id,
                   pId: this.staffmgrList[i].pId,
                   label: this.staffmgrList[i].name,
+                  // 性别:用于前端卡牌配色(2=女)
+                  sex: this.staffmgrList[i].sex,
                   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' + process.env.VUE_APP_BASE_API + this.staffmgrList[i].photo,
@@ -376,10 +380,14 @@ export default {
           }else if ((item.post == '资深工程师' || item.post == '工程师') && map[item.pId].pId == 0) { //直属装置经理的资深工程师
             map[item.pId].secretary ? map[item.pId].secretary[2].push(item) : map[item.pId].secretary[2] = [item];
           } else if (item.post == '片区工长' || item.post == '职员' || item.post == '主操(白班)') {
-            // 如果是直接汇报给资深工程师,则放在资深工程师的下级层级
+            // 结构调整:工长/文员从第三排挪到第二排,并排在资深工程师前面(同一行)
+            // 说明:如果该人员直接汇报给资深工程师/工程师,则仍然放在其 children(保持层级关系不变)
             if (map[item.pId] && (map[item.pId].post == '资深工程师' || map[item.pId].post == '工程师')) {
               map[item.pId].children ? map[item.pId].children.push(item) : map[item.pId].children = [item];
+            } else if (item.post == '片区工长' || item.post == '职员') {
+              map[item.pId].secretary ? map[item.pId].secretary[2].push(item) : map[item.pId].secretary[2] = [item];
             } else {
+              // 主操(白班)仍放第三排
               map[item.pId].secretary ? map[item.pId].secretary[3].push(item) : map[item.pId].secretary[3] = [item];
             }
           } else {
@@ -416,12 +424,16 @@ export default {
             if (b.post === '安全专员') return -1;
             // 其他职位按优先级排序
             const getPriority = (post) => {
+              // 第二排排序:生产主管 -> 工长/片区工长 -> 职员(文员) -> 资深工程师 -> 工程师 -> 其他
+              // 安全专员在上面已强制排到最后
               switch(post) {
-                case '生产主管': return 1;
-                case '工长': return 2;
-                case '资深工程师': return 3;
-                case '工程师': return 4;
-                default: return 5;
+                case '生产主管': return 4;
+                case '工长': return 1;
+                case '片区工长': return 3;
+                case '职员': return 2;
+                case '资深工程师': return 5;
+                case '工程师': return 6;
+                default: return 7;
               }
             };
             return getPriority(a.post) - getPriority(b.post);

+ 11 - 2
ui/src/views/reliability/rel_device/detail.vue

@@ -706,12 +706,21 @@ export default {
 }
 
 .compo-photo {
-  max-width: 100%;
-  max-height: 200px;
+  width: 100%;
+  height: 30vh;
+  max-height: 260px;
+  min-height: 160px;
   border-radius: 4px;
   cursor: pointer;
 }
 
+/* 约束 ElementUI el-image 内部 img,避免大图撑开对话框 */
+.compo-photo /deep/ .el-image__inner {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+}
+
 .image-error {
   display: flex;
   flex-direction: column;

+ 353 - 31
ui/src/views/reliability/rel_device/process-diagram.vue

@@ -6,6 +6,8 @@
 		  <button @click="resetView" class="tool-btn">重置视图</button>
 		  <button @click="zoomIn" class="tool-btn">放大</button>
 		  <button @click="zoomOut" class="tool-btn">缩小</button>
+		  <button @click="toggleOverdueCompoHighlight" class="tool-btn tool-btn-danger" :disabled="overdueLoading">{{ overdueLoading ? '查询中...' : (overdueActive ? `清除超期(${overdueCompoNames.length})` : '超期部件') }}</button>
+		  <button @click="toggleDueSoonCompoHighlight" class="tool-btn tool-btn-warning" :disabled="dueSoonLoading">{{ dueSoonLoading ? '查询中...' : (dueSoonActive ? `清除即将到期(${dueSoonCompoNames.length})` : '即将到期') }}</button>
 		</div>
 		<span v-if="hoverShape && hoverShape.name" class="hover-name">{{ hoverShape.name }}</span>
 	  </div>
@@ -71,6 +73,7 @@
   <script>
   import VueDraggableResizable from 'vue-draggable-resizable'
   import 'vue-draggable-resizable/dist/VueDraggableResizable.css'
+  import { listDueSoonCompos, listOverdueCompos } from '@/api/reliability/rel_device'
 
   export default {
     name: 'ProcessDiagram',
@@ -94,6 +97,14 @@
         scale: 1,
         // 当前悬停的形状(只保留name用于tooltip显示)
         hoverShape: null,
+        // 延期(逾期)部件高亮相关(延期计算在后端,前端只负责高亮展示)
+        overdueActive: false,
+        overdueCompoNames: [],
+        overdueLoading: false,
+		// 即将到期部件高亮相关(“周期最后20%以内 & 但不能延期”在后端计算;前端只负责高亮展示)
+		dueSoonActive: false,
+		dueSoonCompoNames: [],
+		dueSoonLoading: false,
         // SVG 文件映射
         svgMap: {
           '224U': require('@/assets/visio/224U.svg'),
@@ -131,12 +142,121 @@
         }
         return photoPath;
       },
+
+	  // 点击按钮:查询即将到期部件并高亮(再次点击则清除高亮)
+	  toggleDueSoonCompoHighlight() {
+		// 正在请求时禁止重复触发
+		if (this.dueSoonLoading) {
+		  return;
+		}
+		if (this.dueSoonActive) {
+		  this.clearDueSoonCompoHighlight();
+		  return;
+		}
+		this.fetchAndHighlightDueSoonCompos();
+	  },
+
+	  // 从后端查询“即将到期部件”(20%阈值与“但不能延期”在后端计算),并在 SVG 上做黄色闪烁高亮
+	  async fetchAndHighlightDueSoonCompos() {
+		const devTag = this.selectedDevice && this.selectedDevice.devTag ? this.selectedDevice.devTag : '';
+		if (!devTag) {
+		  this.$message && this.$message.warning('缺少设备位号(devTag),无法查询即将到期部件');
+		  return;
+		}
+
+		this.dueSoonLoading = true;
+		try {
+		  const res = await listDueSoonCompos(devTag);
+		  const dueSoonNames = (res && res.data) ? res.data : [];
+
+		  // 每次查询前先清理旧的即将到期样式(避免后端结果变化导致残留)
+		  this.clearDueSoonCompoHighlight();
+
+		  this.dueSoonCompoNames = Array.isArray(dueSoonNames) ? dueSoonNames : [];
+		  if (!this.dueSoonCompoNames.length) {
+			this.$message && this.$message.success('未发现即将到期部件');
+			return;
+		  }
+		  this.dueSoonActive = true;
+		  this.applyDueSoonHighlightByNames(this.dueSoonCompoNames);
+		  this.$message && this.$message.success(`已高亮即将到期部件:${this.dueSoonCompoNames.length} 个`);
+		} catch (e) {
+		  this.$message && this.$message.error('查询即将到期部件失败');
+		} finally {
+		  this.dueSoonLoading = false;
+		}
+	  },
+
+	  // 按“部件名称列表”高亮 SVG 中同名(去掉 .xxx 后缀后匹配)的元素(黄色)
+	  applyDueSoonHighlightByNames(dueSoonNames) {
+		if (!dueSoonNames || !dueSoonNames.length) return;
+		this.$nextTick(() => {
+		  const svgDoc = this.$refs.svgObject?.contentDocument;
+		  if (!svgDoc) return;
+
+		  const groups = svgDoc.querySelectorAll('g[id]');
+		  const dueSoonSet = new Set(dueSoonNames);
+
+		  groups.forEach((g) => {
+			const cleanName = this.getCleanGroupTitle(g);
+			if (!cleanName) return;
+			if (!dueSoonSet.has(cleanName)) return;
+
+			const innerPath = g.querySelector('path, line, rect, circle, polygon');
+			if (!innerPath) return;
+
+			if (!innerPath.getAttribute('data-orig-stroke')) {
+			  this.saveOriginalStyles(innerPath);
+			}
+			this.addTransitionEffect(innerPath);
+			this.applyDueSoonEffect(innerPath);
+		  });
+		});
+	  },
+
+	  // 清除即将到期部件高亮
+	  clearDueSoonCompoHighlight() {
+		this.dueSoonActive = false;
+		this.dueSoonCompoNames = [];
+		const svgDoc = this.$refs.svgObject?.contentDocument;
+		if (!svgDoc) return;
+		const flashingEls = svgDoc.querySelectorAll('.due-soon-flash');
+		flashingEls.forEach((el) => {
+		  this.clearDueSoonEffect(el);
+		});
+	  },
+
+	  // 应用即将到期黄色闪烁效果
+	  applyDueSoonEffect(element) {
+		if (!element) return;
+		element.classList.add('due-soon-flash');
+		element.style.stroke = '#f59e0b';
+		element.style.strokeWidth = '4px';
+		element.style.filter = 'drop-shadow(0 0 6px #f59e0b)';
+	  },
+
+	  // 清除即将到期效果:若仍处于选中状态,则恢复选中样式;否则恢复原始样式
+	  clearDueSoonEffect(element) {
+		if (!element) return;
+		element.classList.remove('due-soon-flash');
+		if (element.classList.contains('selected')) {
+		  this.applySelectedEffect(element);
+		  return;
+		}
+		element.style.opacity = '';
+		element.style.stroke = element.getAttribute('data-orig-stroke') || '';
+		element.style.strokeWidth = element.getAttribute('data-orig-stroke-width') || '';
+		element.style.filter = '';
+	  },
       // 初始化 SVG
       initSvg() {
         this.loading = false;
         const svgDoc = this.$refs.svgObject.contentDocument;
         if (!svgDoc) return;
 
+        // 注入延期闪烁动画样式(只注入一次)
+        this.injectOverdueFlashStyle(svgDoc);
+
         // 所有带 id 的元素(Visio 的管线、设备组)
         const groups = svgDoc.querySelectorAll("g[id]");
 
@@ -290,8 +410,13 @@
 
       // 应用悬停效果
       applyHoverEffect(element) {
+        if (!element) return;
         // 如果元素已被选中,不应用悬停效果
         if (element.classList.contains('selected')) return;
+        // 如果元素正在进行“延期闪烁高亮”,不叠加悬停样式,避免覆盖红色效果
+        if (element.classList.contains('overdue-flash')) return;
+		// 如果元素正在进行“即将到期闪烁高亮”,不叠加悬停样式,避免覆盖黄色效果
+		if (element.classList.contains('due-soon-flash')) return;
 
         const tagName = element.tagName.toLowerCase();
         const origStroke = element.getAttribute("data-orig-stroke") || "#000";
@@ -313,14 +438,171 @@
       clearHoverEffect(element) {
         // 如果元素已被选中,不清除选中样式
         if (element.classList.contains('selected')) return;
+        // 如果元素正在进行“延期闪烁高亮”,不清除红色效果
+        if (element.classList.contains('overdue-flash')) return;
+		// 如果元素正在进行“即将到期闪烁高亮”,不清除黄色效果
+		if (element.classList.contains('due-soon-flash')) return;
 
         element.style.stroke = element.getAttribute("data-orig-stroke") || "";
         element.style.strokeWidth = element.getAttribute("data-orig-stroke-width") || "";
         element.style.filter = "";
       },
 
+      // 仅向 SVG 文档注入一次延期闪烁动画样式(避免 scoped 样式无法作用到 object 内部的 SVG)
+      injectOverdueFlashStyle(svgDoc) {
+        if (!svgDoc) return;
+        if (svgDoc.getElementById('overdue-flash-style')) return;
+        const styleEl = svgDoc.createElement('style');
+        styleEl.setAttribute('id', 'overdue-flash-style');
+        styleEl.textContent = `
+          @keyframes overdueFlashOpacity {
+            0% { opacity: 1; }
+            50% { opacity: 0.35; }
+            100% { opacity: 1; }
+          }
+          .overdue-flash {
+            animation: overdueFlashOpacity 0.9s infinite;
+          }
+		  .due-soon-flash {
+			animation: overdueFlashOpacity 0.9s infinite;
+		  }
+        `;
+        // 放在 svg 根节点下,确保样式在 SVG 内部可用
+        (svgDoc.documentElement || svgDoc).appendChild(styleEl);
+      },
+
+      // 点击按钮:查询延期部件并高亮(再次点击则清除高亮)
+      toggleOverdueCompoHighlight() {
+        // 正在请求时禁止重复触发
+        if (this.overdueLoading) {
+          return;
+        }
+
+        // 已经处于高亮状态:点击则清除(需要“刷新”时可以先清除再点一次)
+        if (this.overdueActive) {
+          this.clearOverdueCompoHighlight();
+          return;
+        }
+
+        this.fetchAndHighlightOverdueCompos();
+      },
+
+      // 从后端查询“延期部件”(延期计算在后端,前端只负责高亮展示),并在 SVG 上做红色闪烁高亮
+      async fetchAndHighlightOverdueCompos() {
+        const devTag = this.selectedDevice && this.selectedDevice.devTag ? this.selectedDevice.devTag : '';
+        if (!devTag) {
+          this.$message && this.$message.warning('缺少设备位号(devTag),无法查询延期部件');
+          return;
+        }
+
+        this.overdueLoading = true;
+        try {
+          const res = await listOverdueCompos(devTag);
+          const overdueNames = (res && res.data) ? res.data : [];
+
+          // 每次查询前先清理旧的延期样式(避免后端结果变化导致残留)
+          this.clearOverdueCompoHighlight();
+
+          this.overdueCompoNames = Array.isArray(overdueNames) ? overdueNames : [];
+          if (!this.overdueCompoNames.length) {
+            this.$message && this.$message.success('未发现超期部件');
+            return;
+          }
+
+          this.overdueActive = true;
+          this.applyOverdueHighlightByNames(this.overdueCompoNames);
+          this.$message && this.$message.success(`已高亮超期部件:${this.overdueCompoNames.length} 个`);
+        } catch (e) {
+          this.$message && this.$message.error('查询超期部件失败');
+        } finally {
+          this.overdueLoading = false;
+        }
+      },
+
+      // 按“部件名称列表”高亮 SVG 中同名(去掉 .xxx 后缀后匹配)的元素
+      applyOverdueHighlightByNames(overdueNames) {
+        if (!overdueNames || !overdueNames.length) return;
+        this.$nextTick(() => {
+          const svgDoc = this.$refs.svgObject?.contentDocument;
+          if (!svgDoc) return;
+
+          const groups = svgDoc.querySelectorAll('g[id]');
+          const overdueSet = new Set(overdueNames);
+
+          groups.forEach((g) => {
+            const cleanName = this.getCleanGroupTitle(g);
+            if (!cleanName) return;
+            if (!overdueSet.has(cleanName)) return;
+
+            const innerPath = g.querySelector('path, line, rect, circle, polygon');
+            if (!innerPath) return;
+
+            if (!innerPath.getAttribute('data-orig-stroke')) {
+              this.saveOriginalStyles(innerPath);
+            }
+            this.addTransitionEffect(innerPath);
+            this.applyOverdueEffect(innerPath);
+          });
+        });
+      },
+
+      // 清除延期部件高亮
+      clearOverdueCompoHighlight() {
+        this.overdueActive = false;
+        this.overdueCompoNames = [];
+        const svgDoc = this.$refs.svgObject?.contentDocument;
+        if (!svgDoc) return;
+
+        // 仅清除延期样式,选中样式(selected)保持不变
+        const flashingEls = svgDoc.querySelectorAll('.overdue-flash');
+        flashingEls.forEach((el) => {
+          this.clearOverdueEffect(el);
+        });
+      },
+
+      // 应用延期闪烁红色效果(参考 hover 的“匹配高亮”思路,但采用红色闪烁)
+      applyOverdueEffect(element) {
+        if (!element) return;
+        element.classList.add('overdue-flash');
+        element.style.stroke = '#ff0000';
+        element.style.strokeWidth = '4px';
+        element.style.filter = 'drop-shadow(0 0 6px #ff0000)';
+      },
+
+      // 清除延期效果:若仍处于选中状态,则恢复选中样式;否则恢复原始样式
+      clearOverdueEffect(element) {
+        if (!element) return;
+        element.classList.remove('overdue-flash');
+        if (element.classList.contains('selected')) {
+          this.applySelectedEffect(element);
+          return;
+        }
+        element.style.opacity = '';
+        element.style.stroke = element.getAttribute('data-orig-stroke') || '';
+        element.style.strokeWidth = element.getAttribute('data-orig-stroke-width') || '';
+        element.style.filter = '';
+      },
+
+      // 获取 SVG 组的 title(去掉 .xxx 后缀)
+      getCleanGroupTitle(group) {
+        const title = group && group.querySelector ? group.querySelector('title')?.textContent : null;
+        if (!title) return null;
+        return title.replace(/\.[^.]+$/, '');
+      },
+
       // 应用选中效果
       applySelectedEffect(element) {
+        // 如果当前元素已经处于“延期闪烁高亮”,则不覆盖为蓝色选中;保持红色闪烁提示
+        if (element && element.classList && element.classList.contains('overdue-flash')) {
+          this.applyOverdueEffect(element);
+          return;
+        }
+		// 如果当前元素已经处于“即将到期闪烁高亮”,则不覆盖为蓝色选中;保持黄色闪烁提示
+		if (element && element.classList && element.classList.contains('due-soon-flash')) {
+		  this.applyDueSoonEffect(element);
+		  return;
+		}
+
         // 使用您提供的高亮样式
         element.style.stroke = "#007bff"; // 高亮蓝色
         element.style.strokeWidth = "4px";
@@ -329,6 +611,17 @@
 
       // 清除选中效果
       clearSelectedEffect(element) {
+        // 如果元素仍处于“延期闪烁高亮”,则恢复延期样式,不恢复原始样式
+        if (element && element.classList && element.classList.contains('overdue-flash')) {
+          this.applyOverdueEffect(element);
+          return;
+        }
+		// 如果元素仍处于“即将到期闪烁高亮”,则恢复即将到期样式,不恢复原始样式
+		if (element && element.classList && element.classList.contains('due-soon-flash')) {
+		  this.applyDueSoonEffect(element);
+		  return;
+		}
+
         // 使用您提供的清除逻辑
         element.style.stroke = element.getAttribute("data-orig-stroke") || "";
         element.style.strokeWidth = element.getAttribute("data-orig-stroke-width") || "";
@@ -391,6 +684,14 @@
       // 监听选中设备变化
       selectedDevice: {
         handler(newDevice) {
+          // 切换设备时,清理上一台设备的“延期闪烁高亮”状态
+          if (this.overdueActive) {
+            this.clearOverdueCompoHighlight();
+          }
+		  // 切换设备时,清理上一台设备的“即将到期闪烁高亮”状态
+		  if (this.dueSoonActive) {
+			this.clearDueSoonCompoHighlight();
+		  }
           if (newDevice && newDevice.devTag) {
             this.highlightDevice(newDevice.devTag);
           } else if (newDevice && newDevice.devName) {
@@ -419,56 +720,77 @@
 
   <style scoped>
   .svg-page {
-	display: flex;
-	flex-direction: column;
-	height: 100vh;
-	background: #f6f8fa;
+    display: flex;
+    flex-direction: column;
+    height: 100vh;
+    background: #f6f8fa;
   }
 
   /* 工具栏样式 */
   .toolbar {
-	display: flex;
-	align-items: center;
-	justify-content: space-between;
-	padding: 10px 20px;
-	background: #ffffff;
-	border-bottom: 1px solid #e5e7eb;
-	box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 10px 20px;
+    background: #ffffff;
+    border-bottom: 1px solid #e5e7eb;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
   }
 
   .toolbar-left {
-	display: flex;
-	gap: 10px;
+    display: flex;
+    gap: 10px;
   }
 
   .hover-name {
-	font-size: 14px;
-	font-weight: 500;
-	color: #3b82f6;
-	background: #eff6ff;
-	padding: 6px 12px;
-	border-radius: 4px;
+    font-size: 14px;
+    font-weight: 500;
+    color: #3b82f6;
+    background: #eff6ff;
+    padding: 6px 12px;
+    border-radius: 4px;
   }
 
   .tool-btn {
-	padding: 8px 16px;
-	background: #3b82f6;
-	color: white;
-	border: none;
-	border-radius: 6px;
-	cursor: pointer;
-	font-size: 14px;
-	transition: background-color 0.2s;
+    padding: 8px 16px;
+    background: #3b82f6;
+    color: white;
+    border: none;
+    border-radius: 6px;
+    cursor: pointer;
+    font-size: 14px;
+    transition: background-color 0.2s;
+  }
+
+  .tool-btn:disabled {
+    opacity: 0.6;
+    cursor: not-allowed;
+  }
+
+  .tool-btn-danger {
+    background: #ef4444;
+  }
+
+  .tool-btn-danger:hover {
+    background: #dc2626;
+  }
+
+  .tool-btn-warning {
+    background: #f59e0b;
   }
 
-  .tool-btn:hover {
-	background: #2563eb;
+  .tool-btn-warning:hover {
+    background: #d97706;
+  }
+
+  .tool-btn:not(.tool-btn-danger):not(.tool-btn-warning):hover {
+    background: #2563eb;
   }
 
   /* 主内容区域 */
   .svg-page > div:not(.toolbar) {
-	display: flex;
-	flex: 1;
+    display: flex;
+    flex: 1;
   }
 
   .svg-container {

+ 43 - 3
ui/src/views/reliability/rel_maint_plan/form.vue

@@ -264,7 +264,7 @@
 import { listRel_device, getRel_device } from "@/api/reliability/rel_device";
 import { listRel_compo } from "@/api/reliability/rel_compo";
 import { listStaffmgrAll } from "@/api/plant/staffmgr";
-import { getRel_maint_plan, submitApprove, resubmitApprove, saveOnlyRel_maint_plan } from "@/api/reliability/rel_maint_plan";
+import { getRel_maint_plan, submitApprove, resubmitApprove, saveOnlyRel_maint_plan, updateRel_maint_plan } from "@/api/reliability/rel_maint_plan";
 import { parseTime as formatTime } from "@/utils/ruoyi";
 
 export default {
@@ -334,6 +334,28 @@ export default {
     });
   },
   methods: {
+    /**
+     * 维修部件校验:只要勾选了“需要维修”,就必须选择维修形式和负责人。
+     * 说明:前端做拦截提升体验,后端也应做同样校验防止绕过。
+     */
+    validateMaintSelection() {
+      const selected = (this.compoList || []).filter(c => c && c.needMaint);
+      if (!selected.length) {
+        return true;
+      }
+
+      for (const compo of selected) {
+        if (!compo.maintType) {
+          this.$message.error(`部件【${compo.compoName || '-'}】已选择需要维修,请先选择维修形式`);
+          return false;
+        }
+        if (!compo.maintResponsible) {
+          this.$message.error(`部件【${compo.compoName || '-'}】已选择需要维修,请先选择负责人`);
+          return false;
+        }
+      }
+      return true;
+    },
     /** 根据员工号获取姓名 */
     getStaffNameById(staffid) {
       if (!staffid) return '';
@@ -402,6 +424,10 @@ export default {
         return;
       }
 
+      if (!this.validateMaintSelection()) {
+        return;
+      }
+
       this.savingOnly = true;
 
       if (this.compoList.length > 0) {
@@ -649,6 +675,11 @@ export default {
           return;
         }
 
+        if (!this.validateMaintSelection()) {
+          this.submitting = false;
+          return;
+        }
+
         if (this.compoList.length > 0) {
           const maintComponents = this.compoList
             .filter(compo => compo.needMaint)
@@ -659,16 +690,25 @@ export default {
               responsible: compo.maintResponsible
             }));
           this.formData.maintComponents = maintComponents;
+        } else {
+          // 保持字段存在,便于后端在更新时做“全量同步/全量删除”判断
+          this.formData.maintComponents = [];
         }
 
         // 选择合适的API
         let apiCall;
-        if (!this.formData.planId || this.isPlannedStatus) {
+        if (!this.formData.planId) {
+          // 新增:提交申请(后端会创建新计划并启动审批流程)
+          apiCall = submitApprove(this.formData);
+        } else if (this.isPlannedStatus) {
+          // 计划中(9):提交申请(后端会创建新计划并删除原“计划中”记录)
           apiCall = submitApprove(this.formData);
         } else if (this.formData.approvalStatus === '1') {
+          // 已通过:再次提交申请(重新启动审批流程)
           apiCall = resubmitApprove(this.formData);
         } else {
-          apiCall = submitApprove(this.formData);
+          // 待审批/进行中修改:直接更新当前计划,不走“创建新计划”的 submitApprove
+          apiCall = updateRel_maint_plan(this.formData);
         }
 
         apiCall.then(response => {

+ 81 - 58
ui/src/views/reliability/rel_maint_record/myRecord.vue

@@ -299,33 +299,6 @@
           <el-form-item label="检查内容" prop="inspectContent">
             <el-input v-model="processForm.inspectContent" type="textarea" :rows="5" placeholder="请输入检查内容" />
           </el-form-item>
-          <el-form-item label="测厚记录" v-if="processForm.thicknessList && processForm.thicknessList.length">
-            <el-table :data="processForm.thicknessList" border size="small">
-              <el-table-column label="测厚点" prop="thicknessPt" width="160" :show-overflow-tooltip="true" />
-              <el-table-column label="测厚日期" width="160">
-                <template slot-scope="scope">
-                  <el-date-picker
-                    clearable
-                    style="width: 100%"
-                    v-model="scope.row.thicknessDate"
-                    type="date"
-                    value-format="yyyy-MM-dd"
-                    placeholder="选择日期">
-                  </el-date-picker>
-                </template>
-              </el-table-column>
-              <el-table-column label="测厚数据" width="180">
-                <template slot-scope="scope">
-                  <el-input-number
-                    v-model="scope.row.thicknessData"
-                    :min="0"
-                    :precision="3"
-                    :step="0.001"
-                    style="width: 100%" />
-                </template>
-              </el-table-column>
-            </el-table>
-          </el-form-item>
           <el-form-item label="是否需要维修/更换">
             <el-checkbox v-model="processForm.needMaintOrReplace">发现需要维修或更换</el-checkbox>
           </el-form-item>
@@ -387,6 +360,34 @@
           <el-form-item label="备注" prop="remarks">
             <el-input v-model="processForm.remarks" type="textarea" :rows="3" placeholder="请输入备注" />
           </el-form-item>
+
+          <el-form-item label="测厚记录" v-if="processForm.thicknessList && processForm.thicknessList.length">
+            <el-table :data="processForm.thicknessList" border size="small">
+              <el-table-column label="测厚点" prop="thicknessPt" width="160" :show-overflow-tooltip="true" />
+              <el-table-column label="测厚日期" width="160">
+                <template slot-scope="scope">
+                  <el-date-picker
+                    clearable
+                    style="width: 100%"
+                    v-model="scope.row.thicknessDate"
+                    type="date"
+                    value-format="yyyy-MM-dd"
+                    placeholder="选择日期">
+                  </el-date-picker>
+                </template>
+              </el-table-column>
+              <el-table-column label="测厚数据(mm)" width="180">
+                <template slot-scope="scope">
+                  <el-input-number
+                    v-model="scope.row.thicknessData"
+                    :min="0"
+                    :precision="2"
+                    :step="0.01"
+                    style="width: 100%" />
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-form-item>
         </div>
       </el-form>
       <div slot="footer" class="dialog-footer">
@@ -447,7 +448,10 @@
           </template>
           <template v-else>
             <el-form-item label="维修部门" prop="maintDept">
-              <el-input v-model="processForm.maintDept" placeholder="请输入维修部门" />
+              <el-radio-group v-model="processForm.maintDept">
+                <el-radio label="CTA">CTA</el-radio>
+                <el-radio label="CTM">CTM</el-radio>
+              </el-radio-group>
             </el-form-item>
             <el-form-item label="维修内容" prop="maintContent">
               <el-input v-model="processForm.maintContent" type="textarea" :rows="5" placeholder="请输入维修内容" />
@@ -505,33 +509,6 @@
               </el-col>
             </el-row>
           </template>
-          <el-form-item label="测厚记录" v-if="processForm.thicknessList && processForm.thicknessList.length">
-            <el-table :data="processForm.thicknessList" border size="small">
-              <el-table-column label="测厚点" prop="thicknessPt" width="160" :show-overflow-tooltip="true" />
-              <el-table-column label="测厚日期" width="160">
-                <template slot-scope="scope">
-                  <el-date-picker
-                    clearable
-                    style="width: 100%"
-                    v-model="scope.row.thicknessDate"
-                    type="date"
-                    value-format="yyyy-MM-dd"
-                    placeholder="选择日期">
-                  </el-date-picker>
-                </template>
-              </el-table-column>
-              <el-table-column label="测厚数据" width="180">
-                <template slot-scope="scope">
-                  <el-input-number
-                    v-model="scope.row.thicknessData"
-                    :min="0"
-                    :precision="3"
-                    :step="0.001"
-                    style="width: 100%" />
-                </template>
-              </el-table-column>
-            </el-table>
-          </el-form-item>
           <el-form-item label="照片" prop="photoUrl">
             <el-upload
               class="upload-demo"
@@ -567,6 +544,34 @@
           <el-form-item label="备注" prop="remarks">
             <el-input v-model="processForm.remarks" type="textarea" :rows="3" placeholder="请输入备注" />
           </el-form-item>
+
+          <el-form-item label="测厚记录" v-if="processForm.thicknessList && processForm.thicknessList.length">
+            <el-table :data="processForm.thicknessList" border size="small">
+              <el-table-column label="测厚点" prop="thicknessPt" width="160" :show-overflow-tooltip="true" />
+              <el-table-column label="测厚日期" width="160">
+                <template slot-scope="scope">
+                  <el-date-picker
+                    clearable
+                    style="width: 100%"
+                    v-model="scope.row.thicknessDate"
+                    type="date"
+                    value-format="yyyy-MM-dd"
+                    placeholder="选择日期">
+                  </el-date-picker>
+                </template>
+              </el-table-column>
+              <el-table-column label="测厚数据(mm)" width="180">
+                <template slot-scope="scope">
+                  <el-input-number
+                    v-model="scope.row.thicknessData"
+                    :min="0"
+                    :precision="2"
+                    :step="0.01"
+                    style="width: 100%" />
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-form-item>
         </div>
       </el-form>
       <div slot="footer" class="dialog-footer">
@@ -655,7 +660,7 @@
             <span>{{ parseTime(scope.row.thicknessDate, '{y}-{m}-{d}') }}</span>
           </template>
         </el-table-column>
-        <el-table-column label="测厚数据" prop="thicknessData" width="120" align="center" />
+        <el-table-column label="测厚数据(mm)" prop="thicknessData" width="120" align="center" />
         <el-table-column label="备注" prop="remarks" :show-overflow-tooltip="true" />
       </el-table>
       <div v-if="thicknessViewList.length === 0" style="text-align: center; color: #999; padding: 20px;">暂无测厚记录</div>
@@ -848,8 +853,14 @@ export default {
         const pt = row ? row.thicknessPt : '';
         const date = row ? row.thicknessDate : null;
         const val = row ? row.thicknessData : null;
-        if (!date || val === null || val === undefined || val === '') {
-          this.$message.error((pt ? `请填写测厚记录:${pt}` : '请填写测厚记录') + '(日期和数值必填)');
+        // 测厚记录不必填:如果该行完全未填写(日期和值都空)则跳过;若填写了一项,则要求两项都填写
+        const hasDate = !!date;
+        const hasVal = !(val === null || val === undefined || val === '');
+        if (!hasDate && !hasVal) {
+          continue;
+        }
+        if (!(hasDate && hasVal)) {
+          this.$message.error((pt ? `请完整填写测厚记录:${pt}` : '请完整填写测厚记录') + '(日期和数值需同时填写)');
           return false;
         }
       }
@@ -1090,6 +1101,18 @@ export default {
           if (!this.validateThicknessList()) {
             return;
           }
+
+          // 测厚记录不必填:提交时只上传“已填写完整(日期+数值)”的行,避免把空行写入数据库
+          if (Array.isArray(this.processForm.thicknessList) && this.processForm.thicknessList.length > 0) {
+            this.processForm.thicknessList = this.processForm.thicknessList
+              .filter(r => r && r.thicknessDate && !(r.thicknessData === null || r.thicknessData === undefined || r.thicknessData === ''))
+              .map(r => ({
+                thicknessPt: r.thicknessPt,
+                thicknessDate: r.thicknessDate,
+                thicknessData: r.thicknessData
+              }));
+          }
+
           // 更新维修状态为已完成
           this.processForm.recordStatus = 2;
           // 将是否需要创建新记录的信息传递给后端