liuwei před 2 měsíci
rodič
revize
918a991499

+ 9 - 0
src/main/java/com/jeesite/modules/mes/dao/MesProductRecordDao.java

@@ -47,4 +47,13 @@ public interface MesProductRecordDao extends CrudDao<MesProductRecord> {
             @Param("startTime") Date startTime,
             @Param("endTime") Date endTime);
 
+    /**
+     * 统计指定产线、工位前缀、时间段内 content=OK 的条数
+     */
+    Long countOkByLineAndOprnoPrefix(
+            @Param("lineSn") String lineSn,
+            @Param("oprnoPrefix") String oprnoPrefix,
+            @Param("startTime") Date startTime,
+            @Param("endTime") Date endTime);
+
 }

+ 10 - 0
src/main/java/com/jeesite/modules/mes/dao/MesRepairDao.java

@@ -24,4 +24,14 @@ public interface MesRepairDao extends CrudDao<MesRepair> {
     List<Map<String, Object>> countGroupByItemTitle(
             @Param("sdate") String sdate,
             @Param("edate") String edate);
+
+    /**
+     * 按产线、时间段、责任工位号数值区间统计返工条数
+     */
+    Long countByLineAndDateAndOprnoNumRange(
+            @Param("lineSn") String lineSn,
+            @Param("sdate") String sdate,
+            @Param("edate") String edate,
+            @Param("fromNum") int fromNum,
+            @Param("toNum") int toNum);
 }

+ 56 - 0
src/main/java/com/jeesite/modules/mes/service/MesProductRecordService.java

@@ -1446,6 +1446,62 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 		return ret;
 	}
 
+	/**
+	 * 固定两班制当班产能:08:00–20:00 或 20:00–次日 08:00
+	 */
+	public Map<String, Integer> getFixedTwoShiftCapacityStats(String lineSn, List<String> oprnos) {
+		Map<String, Integer> ret = MapUtils.newHashMap();
+		if (StringUtils.isBlank(lineSn) || oprnos == null || oprnos.isEmpty()) {
+			return ret;
+		}
+		Calendar now = Calendar.getInstance();
+		int hour = now.get(Calendar.HOUR_OF_DAY);
+		Calendar startCal = Calendar.getInstance();
+		Calendar endCal = Calendar.getInstance();
+		startCal.set(Calendar.MINUTE, 0);
+		startCal.set(Calendar.SECOND, 0);
+		startCal.set(Calendar.MILLISECOND, 0);
+		endCal.set(Calendar.MINUTE, 0);
+		endCal.set(Calendar.SECOND, 0);
+		endCal.set(Calendar.MILLISECOND, 0);
+		if (hour >= 8 && hour < 20) {
+			startCal.set(Calendar.HOUR_OF_DAY, 8);
+			endCal.set(Calendar.HOUR_OF_DAY, 20);
+		} else if (hour >= 20) {
+			startCal.set(Calendar.HOUR_OF_DAY, 20);
+			endCal.add(Calendar.DAY_OF_MONTH, 1);
+			endCal.set(Calendar.HOUR_OF_DAY, 8);
+		} else {
+			endCal.set(Calendar.HOUR_OF_DAY, 8);
+			startCal.add(Calendar.DAY_OF_MONTH, -1);
+			startCal.set(Calendar.HOUR_OF_DAY, 20);
+		}
+		Date startTime = startCal.getTime();
+		Date endTime = endCal.getTime();
+		for (String oprno : oprnos) {
+			if (StringUtils.isBlank(oprno)) {
+				continue;
+			}
+			String formatOprno = CommonUitl.formatOprno(oprno.trim());
+			Long c = mesProductRecordDao.countHourCapacityByLineAndOprnoPrefix(
+					lineSn.trim(), formatOprno, startTime, endTime);
+			ret.put(formatOprno, c == null ? 0 : c.intValue());
+		}
+		return ret;
+	}
+
+	/**
+	 * 指定产线、工位前缀、时间段内 content=OK 的数量
+	 */
+	public int countOkByLineAndOprnoPrefix(String lineSn, String oprnoPrefix, Date startTime, Date endTime) {
+		if (StringUtils.isBlank(lineSn) || StringUtils.isBlank(oprnoPrefix) || startTime == null || endTime == null) {
+			return 0;
+		}
+		Long c = mesProductRecordDao.countOkByLineAndOprnoPrefix(
+				lineSn.trim(), CommonUitl.formatOprno(oprnoPrefix.trim()), startTime, endTime);
+		return c == null ? 0 : c.intValue();
+	}
+
 	private void fillHourlyCounts(List<Map<String, Object>> rows, int[] bucket) {
 		if (rows == null) {
 			return;

+ 8 - 0
src/main/java/com/jeesite/modules/mes/service/MesRepairService.java

@@ -134,4 +134,12 @@ public class MesRepairService extends CrudService<MesRepairDao, MesRepair> {
 				.and("create_date", QueryType.LT, edate);
 		return super.findCount(q);
 	}
+
+	/**
+	 * 统计指定产线、时间段内责任工位号在 [fromNum, toNum] 闭区间的返工条数
+	 */
+	public Long countByLineAndDateAndOprnoNumRange(String lineSn, String sdate, String edate, int fromNum, int toNum) {
+		Long c = dao.countByLineAndDateAndOprnoNumRange(lineSn, sdate, edate, fromNum, toNum);
+		return c == null ? 0L : c;
+	}
 }

+ 62 - 10
src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java

@@ -442,7 +442,8 @@ public class MesProductRecordController extends BaseController {
 	}
 
 	/**
-	 * 当班产能:根据班次生产计划管理的当前班次时间段统计指定产线、各工位的产能
+	 * 当班产能:根据班次生产计划管理的当前班次时间段统计;
+	 * 传 fixedTwoShift=true 时改为固定两班 08:00–20:00 / 20:00–次日 08:00
 	 */
 	@RequestMapping(value = "shiftCapacityData")
 	@ResponseBody
@@ -450,6 +451,7 @@ public class MesProductRecordController extends BaseController {
 		CommonResp<Map<String, Integer>> resp = new CommonResp<>();
 		String lineSn = StringUtils.trimToEmpty(request.getParameter("lineSn"));
 		String oprnos = StringUtils.trimToEmpty(request.getParameter("oprnos"));
+		String fixedTwoShift = StringUtils.trimToEmpty(request.getParameter("fixedTwoShift"));
 		if (StringUtils.isBlank(lineSn) || StringUtils.isBlank(oprnos)) {
 			resp.setResult("false");
 			resp.setMessage("lineSn、oprnos 不能为空");
@@ -464,7 +466,10 @@ public class MesProductRecordController extends BaseController {
 					oprnoList.add(oprno);
 				}
 			}
-			resp.setData(mesProductRecordService.getCurrentShiftCapacityStats(lineSn, oprnoList));
+			boolean useFixed = "true".equalsIgnoreCase(fixedTwoShift) || "1".equals(fixedTwoShift);
+			resp.setData(useFixed
+					? mesProductRecordService.getFixedTwoShiftCapacityStats(lineSn, oprnoList)
+					: mesProductRecordService.getCurrentShiftCapacityStats(lineSn, oprnoList));
 			resp.setResult("true");
 		} catch (Exception e) {
 			logger.error(e.getMessage(), e);
@@ -476,25 +481,29 @@ public class MesProductRecordController extends BaseController {
 
 	/**
 	 * 近七日产品质检数据分析(按产线)
-	 * 产出:半成品入库管理(mes_inbound) 按产线、按天统计的入库数量
-	 * 不良:返工记录表(mes_repair) 按产线、按天统计的返工数量
-	 * 返回最近 7 天(含今天)的数据,labels/produced/defect 三个数组一一对应
+	 * 默认:产出=半成品入库,不良=返工全量
+	 * 可选 producedOprno:产出改为该工位 content=OK 按日计数
+	 * 可选 defectOprnoFrom/defectOprnoTo:不良改为责任工位号数值闭区间
 	 */
 	@RequestMapping(value = "weekQualityData")
 	@ResponseBody
 	public CommonResp<Map<String, Object>> weekQualityData(HttpServletRequest request) {
 		CommonResp<Map<String, Object>> resp = new CommonResp<>();
 		String lineSn = StringUtils.trimToEmpty(request.getParameter("lineSn"));
+		String producedOprno = StringUtils.trimToEmpty(request.getParameter("producedOprno"));
+		String defectFromStr = StringUtils.trimToEmpty(request.getParameter("defectOprnoFrom"));
+		String defectToStr = StringUtils.trimToEmpty(request.getParameter("defectOprnoTo"));
 		if (StringUtils.isBlank(lineSn)) {
 			resp.setResult("false");
 			resp.setMessage("lineSn 不能为空");
 			return resp;
 		}
 		try {
+			Integer defectFrom = parseOprnoNum(defectFromStr);
+			Integer defectTo = parseOprnoNum(defectToStr);
 			List<String> labels = new ArrayList<>();
 			List<Integer> produced = new ArrayList<>();
 			List<Integer> defect = new ArrayList<>();
-			SimpleDateFormat dayFmt = new SimpleDateFormat("yyyy-MM-dd");
 			Calendar cal = Calendar.getInstance();
 			cal.set(Calendar.HOUR_OF_DAY, 0);
 			cal.set(Calendar.MINUTE, 0);
@@ -512,10 +521,25 @@ public class MesProductRecordController extends BaseController {
 				int d = dayStartCal.get(Calendar.DAY_OF_MONTH);
 				labels.add(m + "月" + d + "日");
 
-				Long outNum = mesInboundService.countByLineAndDate(lineSn, dayStartStr, dayEndStr);
-				Long ngNum = mesRepairService.countByLineAndDate(lineSn, dayStartStr, dayEndStr);
-				produced.add(outNum == null ? 0 : outNum.intValue());
-				defect.add(ngNum == null ? 0 : ngNum.intValue());
+				int outNum;
+				if (StringUtils.isNotBlank(producedOprno)) {
+					outNum = mesProductRecordService.countOkByLineAndOprnoPrefix(
+							lineSn, producedOprno, dayStartCal.getTime(), dayEndCal.getTime());
+				} else {
+					Long inbound = mesInboundService.countByLineAndDate(lineSn, dayStartStr, dayEndStr);
+					outNum = inbound == null ? 0 : inbound.intValue();
+				}
+				int ngNum;
+				if (defectFrom != null && defectTo != null) {
+					Long ng = mesRepairService.countByLineAndDateAndOprnoNumRange(
+							lineSn, dayStartStr, dayEndStr, defectFrom, defectTo);
+					ngNum = ng == null ? 0 : ng.intValue();
+				} else {
+					Long ng = mesRepairService.countByLineAndDate(lineSn, dayStartStr, dayEndStr);
+					ngNum = ng == null ? 0 : ng.intValue();
+				}
+				produced.add(outNum);
+				defect.add(ngNum);
 
 				cal.add(Calendar.DAY_OF_MONTH, 1);
 			}
@@ -533,6 +557,34 @@ public class MesProductRecordController extends BaseController {
 		return resp;
 	}
 
+	/** 从 OP070 / OP100 解析工位数字;无法解析返回 null */
+	private static Integer parseOprnoNum(String oprno) {
+		if (StringUtils.isBlank(oprno)) {
+			return null;
+		}
+		String s = oprno.trim().toUpperCase();
+		if (s.startsWith("OP")) {
+			s = s.substring(2);
+		}
+		StringBuilder digits = new StringBuilder();
+		for (int i = 0; i < s.length(); i++) {
+			char c = s.charAt(i);
+			if (c >= '0' && c <= '9') {
+				digits.append(c);
+			} else {
+				break;
+			}
+		}
+		if (digits.length() == 0) {
+			return null;
+		}
+		try {
+			return Integer.parseInt(digits.toString());
+		} catch (NumberFormatException e) {
+			return null;
+		}
+	}
+
 	/**
 	 * 各月产量统计图(按产线)
 	 * 按月统计半成品入库管理(mes_inbound) 的入库数量,返回当年 1~12 月数据

+ 10 - 0
src/main/resources/mappings/modules/mes/MesProductRecordDao.xml

@@ -75,4 +75,14 @@
           AND UPPER(TRIM(IFNULL(a.content, ''))) != 'UDF'
           AND a.oprno LIKE CONCAT(#{oprnoPrefix}, '%')
     </select>
+
+    <select id="countOkByLineAndOprnoPrefix" resultType="java.lang.Long">
+        SELECT COUNT(1)
+        FROM mes_product_record a
+        WHERE a.update_date <![CDATA[ >= ]]> #{startTime}
+          AND a.update_date <![CDATA[ < ]]> #{endTime}
+          AND a.line_sn = #{lineSn}
+          AND UPPER(TRIM(IFNULL(a.content, ''))) = 'OK'
+          AND a.oprno LIKE CONCAT(#{oprnoPrefix}, '%')
+    </select>
 </mapper>

+ 9 - 0
src/main/resources/mappings/modules/mes/MesRepairDao.xml

@@ -39,4 +39,13 @@
 		GROUP BY b.title
 		ORDER BY cnt DESC
 	</select>
+
+	<select id="countByLineAndDateAndOprnoNumRange" resultType="java.lang.Long">
+		SELECT COUNT(1)
+		FROM mes_repair a
+		WHERE a.line_sn = #{lineSn}
+		  AND a.create_date &gt;= #{sdate}
+		  AND a.create_date &lt; #{edate}
+		  AND CAST(SUBSTRING(a.oprno, 3) AS UNSIGNED) BETWEEN #{fromNum} AND #{toNum}
+	</select>
 </mapper>

+ 152 - 273
src/main/resources/views/modules/mes/mesScreen4.html

@@ -40,34 +40,34 @@
 		.mes4-title {
 			position: absolute;
 			left: 50%;
-			top: 20px;
+			top: 18px;
 			transform: translateX(-50%);
-			font-size: 38px;
+			font-size: 42px;
 			font-weight: bold;
 			color: #FFFFFF;
-			letter-spacing: 3px;
+			letter-spacing: 4px;
 			text-shadow: 0 0 10px rgba(77, 201, 245, 0.6);
 			white-space: nowrap;
 		}
 		.mes4-datetime {
 			position: absolute;
-			top: 32px;
+			top: 30px;
 			right: 50px;
 			text-align: right;
 		}
 		.mes4-datetime .mes4-date {
 			display: inline-block;
-			font-size: 18px;
+			font-size: 20px;
 			color: #FFFFFF;
-			line-height: 28px;
+			line-height: 30px;
 		}
 		.mes4-datetime .mes4-time {
 			display: inline-block;
 			margin-left: 14px;
-			font-size: 22px;
+			font-size: 26px;
 			color: #FFFFFF;
 			font-weight: bold;
-			line-height: 28px;
+			line-height: 30px;
 		}
 
 		/* ===== 面板通用 - 使用自定义类名避免与 Bootstrap 的 .panel 冲突 ===== */
@@ -79,107 +79,111 @@
 			box-shadow: none;
 		}
 		.mes4-panel .mes4-ptitle {
-			height: 42px;
-			line-height: 42px;
+			height: 48px;
+			line-height: 48px;
 			text-align: center;
-			font-size: 18px;
+			font-size: 22px;
 			font-weight: bold;
 			color: #4DC9F5;
-			letter-spacing: 1px;
+			letter-spacing: 2px;
 			background: transparent;
 		}
 		.mes4-panel .mes4-pbody {
 			position: absolute;
-			top: 42px;
+			top: 48px;
 			left: 0;
 			right: 0;
 			bottom: 0;
-			padding: 5px 10px;
+			padding: 8px 14px;
 			background: transparent;
 		}
 		.chart { width: 100%; height: 100%; display: block; }
 
-		/* ===== 位置定义 (1920 × 1080 画布) ===== */
-		/* 左列: x=30, 宽=430 */
-		.p-alert    { left: 40px;  top: 130px; width: 430px; height: 290px; }
-		.p-quality  { left: 40px;  top: 455px; width: 430px; height: 265px; }
-		.p-product  { left: 40px;  top: 763px; width: 430px; height: 306px; }
+		/* ===== 位置:左右对齐,中间表按 5 行收短 ===== */
+		.p-alert    { left: 36px;  top: 118px; width: 440px; height: 430px; }
+		.p-quality  { left: 36px;  top: 568px; width: 440px; height: 460px; }
+		.p-product  { display: none; }
 
-		/* 中列: x=480, 宽=960 */
-		.p-table    { left: 480px; top: 130px; width: 960px; height: 575px; }
-		.p-month    { left: 480px; top: 725px; width: 960px; height: 306px; }
+		.p-table    { left: 496px; top: 118px; width: 928px; height: 560px; }
+		.p-month    { display: none; }
 
-		/* 右列: right=30, 宽=430 */
 		.p-overview { right: 30px; top: 130px; width: 430px; height: 290px; }
-		.p-achieve  { right: 40px; top: 455px; width: 430px; height: 265px; }
-		.p-hourly   { right: 40px; top: 763px; width: 430px; height: 306px; }
+		.p-achieve  { display: none; }
+		.p-hourly   { right: 36px; top: 568px; width: 440px; height: 460px; }
 
-		/* 阶段报警信息 - 按模板比例调整 */
-		.p-alert .mes4-ptitle {
+		.p-alert .mes4-ptitle,
+		.p-overview .mes4-ptitle,
+		.p-quality .mes4-ptitle,
+		.p-hourly .mes4-ptitle {
 			height: 40px;
 			line-height: 40px;
 			font-size: 16px;
-			color: #4DC9F5;
-			font-weight: bold;
-			letter-spacing: 1px;
 		}
-		.p-alert .mes4-pbody  { top: 40px; padding: 0; }
-		/* 总览标题 */
-		.p-overview .mes4-ptitle { height: 40px; line-height: 40px; font-size: 16px; }
-		.p-overview .mes4-pbody  { top: 40px; }
+		.p-alert .mes4-pbody { top: 48px; padding: 0 6px 8px; }
+		.p-overview .mes4-pbody { top: 40px; padding: 0; }
+		.p-table .mes4-pbody { top: 8px; padding: 8px 28px 12px; }
+		.p-quality .mes4-pbody,
+		.p-hourly .mes4-pbody { top: 72px; }
 
 		/* ===== 报警信息 ===== */
 		.alert-head {
 			display: flex;
 			align-items: center;
-			height: 36px;
-			padding: 0 22px;
+			height: 42px;
+			padding: 0 20px;
 			color: #4DC9F5;
-			font-size: 14px;
+			font-size: 16px;
 			font-weight: bold;
-			border-bottom: 1px solid rgba(77, 201, 245, 0.3);
+			border-bottom: 2px solid rgba(77, 201, 245, 0.45);
 		}
-		.alert-head > div:nth-child(1) { width: 70px; }
+		.alert-head > div:nth-child(1) { width: 72px; }
 		.alert-head > div:nth-child(2) { flex: 1; }
 		.alert-row {
 			display: flex;
 			align-items: center;
-			height: 28px;
-			padding: 0 22px;
+			height: 32px;
+			padding: 0 20px;
 			color: #ffffff;
-			font-size: 13px;
+			font-size: 15px;
+			border-bottom: 1px solid rgba(77, 201, 245, 0.12);
 		}
-		.alert-row > div:nth-child(1) { width: 70px; color: #4DC9F5; font-weight: bold; }
-		.alert-row > div:nth-child(2) { flex: 1; }
+		.alert-row > div:nth-child(1) { width: 72px; color: #4DC9F5; font-weight: bold; font-size: 16px; }
+		.alert-row > div:nth-child(2) { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
 
-		/* ===== OP 表格 ===== */
+		/* ===== OP 表格(5 行加高、字号加大) ===== */
 		.op-table {
 			width: 100%;
+			height: 100%;
 			border-collapse: collapse;
 			color: #ffffff;
-			font-size: 16px;
+			font-size: 20px;
 			table-layout: fixed;
 		}
 		.op-table thead th {
-			height: 46px;
+			height: 58px;
 			color: #4DC9F5;
-			font-weight: normal;
-			font-size: 16px;
-			background: rgba(0, 100, 180, 0.25);
-			border-bottom: 1px solid rgba(77, 201, 245, 0.35);
+			font-weight: bold;
+			font-size: 20px;
+			letter-spacing: 1px;
+			background: rgba(0, 100, 180, 0.28);
+			border-bottom: 2px solid rgba(77, 201, 245, 0.5);
 			text-align: center;
 		}
 		.op-table tbody td {
-			height: 46px;
+			height: 88px;
 			text-align: center;
-			border-bottom: 1px dashed rgba(77, 201, 245, 0.18);
+			border-bottom: 1px solid rgba(77, 201, 245, 0.22);
+			vertical-align: middle;
+		}
+		.op-table tbody tr:last-child td {
+			border-bottom: 2px solid rgba(77, 201, 245, 0.35);
 		}
 		.op-table tbody tr:hover td { background: rgba(0, 200, 255, 0.06); }
-		.op-table td.op-col  { color: #00C8FF; font-weight: bold; font-size: 18px; }
-		.op-table td.op-name { color: #ffffff; text-align: center; }
-		.op-table td.op-val  { color: #00F7FF; font-weight: bold; font-size: 18px; }
+		.op-table td.op-col  { color: #00C8FF; font-weight: bold; font-size: 26px; }
+		.op-table td.op-name { color: #ffffff; text-align: center; font-size: 22px; }
+		.op-table td.op-val  { color: #00F7FF; font-weight: bold; font-size: 28px; }
 
-		/* ===== 产线阶段数据总览 (按模板排布) ===== */
+		/* ===== 产线阶段数据总览(对齐原 UI 四宫格框) ===== */
 		.overview-wrap {
 			position: relative;
 			height: 100%;
@@ -211,11 +215,6 @@
 			font-weight: bold;
 			line-height: 1;
 		}
-		.overview-card .oc-value .arrow {
-			font-size: 13px;
-			color: #00FF88;
-			margin-left: 3px;
-		}
 		.overview-gauge-wrap {
 			position: absolute;
 			top: 50%;
@@ -250,17 +249,17 @@
 			display: flex;
 			align-items: center;
 			justify-content: flex-end;
-			padding: 4px 18px 0;
-			font-size: 13px;
+			padding: 6px 22px 0;
+			font-size: 15px;
 			color: #ffffff;
-			gap: 18px;
+			gap: 22px;
 		}
 		.legend-inline .dot {
 			display: inline-block;
 			width: 12px;
 			height: 12px;
 			border-radius: 50%;
-			margin-right: 5px;
+			margin-right: 6px;
 			vertical-align: middle;
 		}
 	</style>
@@ -279,7 +278,7 @@
 
 	<div class="mes4-panel p-alert">
 		<div class="mes4-ptitle">  产线阶段报警信息  </div>
-		<div class="mes4-pbody" style="padding:0">
+		<div class="mes4-pbody">
 			<div class="alert-head"><div>序号</div><div>报警类型</div></div>
 			<div id="alertList"></div>
 		</div>
@@ -291,7 +290,7 @@
 			<span><i class="dot" style="background:#00FF88"></i>产出</span>
 			<span><i class="dot" style="background:#00C8FF"></i>不良</span>
 		</div>
-		<div class="mes4-pbody" style="top:62px"><canvas id="chartQuality" class="chart"></canvas></div>
+		<div class="mes4-pbody"><canvas id="chartQuality" class="chart"></canvas></div>
 	</div>
 
 	<div class="mes4-panel p-product">
@@ -300,16 +299,14 @@
 	</div>
 
 	<div class="mes4-panel p-table">
-		<div class="mes4-pbody" style="top:5px;padding:0 20px">
+		<div class="mes4-pbody">
 			<table class="op-table">
 				<thead>
 				<tr>
-					<th style="width:110px">工位</th>
-					<th style="width:200px">工位名称</th>
-					<th>计划产能</th>
-					<th>小时产能</th>
+					<th style="width:160px">工位</th>
+					<th style="width:300px">工位名称</th>
 					<th>实际产能</th>
-					<th>当班产能(12小时内)</th>
+					<th>当班产能</th>
 				</tr>
 				</thead>
 				<tbody id="opTableBody"></tbody>
@@ -329,26 +326,19 @@
 				<div class="overview-cards">
 					<div class="overview-card">
 						<div class="oc-label">生产人数</div>
-						<div class="oc-value"><span id="productionPeople">48</span> <span class="arrow">↑</span></div>
+						<div class="oc-value"><span id="productionPeople">48</span></div>
 					</div>
 					<div class="overview-card">
 						<div class="oc-label">设备数量</div>
-						<div class="oc-value"><span id="deviceCount">56</span> <span class="arrow">↑</span></div>
+						<div class="oc-value"><span id="deviceCount">56</span></div>
 					</div>
 					<div class="overview-card">
 						<div class="oc-label">计划产量</div>
-						<div class="oc-value"><span id="planProduction">221</span> <span class="arrow">↑</span></div>
+						<div class="oc-value"><span id="planProduction">221</span></div>
 					</div>
 					<div class="overview-card">
 						<div class="oc-label">实际产量</div>
-						<div class="oc-value"><span id="actualProduction">201</span> <span class="arrow">↑</span></div>
-					</div>
-				</div>
-				<div class="overview-gauge-wrap">
-					<canvas id="gaugeRate"></canvas>
-					<div class="gauge-txt">
-						<div class="val"><span id="gaugeVal">70</span>%</div>
-						<div class="lbl">今日达成率</div>
+						<div class="oc-value"><span id="actualProduction">201</span></div>
 					</div>
 				</div>
 			</div>
@@ -370,7 +360,7 @@
 			<span><i class="dot" style="background:#00FF88"></i>上线</span>
 			<span><i class="dot" style="background:#00C8FF"></i>下线</span>
 		</div>
-		<div class="mes4-pbody" style="top:62px"><canvas id="chartHourly" class="chart"></canvas></div>
+		<div class="mes4-pbody"><canvas id="chartHourly" class="chart"></canvas></div>
 	</div>
 </div>
 
@@ -390,10 +380,12 @@
 		document.getElementById('mes4Time').textContent = pad(h) + ':' + pad(mi) + ':' + pad(s);
 	}
 
-	// ====== 测试数据 ======
+	// ====== 页面数据 ======
 	var mockData = {};
-	/** 每小时上下线:mesProduct/hourlyUpDownOkData(本页 lineSn=MP,上线 OP120 / 下线 OP190) */
-	var HOURLY_API_CFG = { lineSn: 'MP', onlineOprno: 'OP120', offlineOprno: 'OP190' };
+	/** 本屏产线:XT(工位 OP060–OP100,与 mesLineProcess 一致) */
+	var LINE_SN = 'XT';
+	/** 每小时上下线:上线 OP060 / 下线 OP100 */
+	var HOURLY_API_CFG = { lineSn: LINE_SN, onlineOprno: 'OP060', offlineOprno: 'OP100' };
 	function buildHourLabels24() {
 		var hourLabels = [];
 		for (var i = 1; i <= 24; i++) {
@@ -438,24 +430,13 @@
 			weekLabels.push((d.getMonth() + 1) + '月' + d.getDate() + '日');
 		}
 
-		// 报警信息由 updateDeviceAlarmAlertsByApi 拉取 mesDeviceAlarm/listData,展示 type
+		// 报警信息由 updateDeviceAlarmAlertsByApi 拉取
 		mockData.alerts = [];
 
-		// OP 表格
-		mockData.opTable = [
-			{ code: 'OP10', name: '上料+打码',       plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP20', name: '底板组件-正面FSW', plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP25', name: '底板组件-打磨',    plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP30', name: '底板组件-反面FSW', plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP40', name: '底板组件-CNC',     plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP50', name: '干冰清洁',         plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP55', name: '模组梁+封板预装',  plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP60', name: '封板正面FSW',      plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP65', name: '打磨',            plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP70', name: '封板反面FSW',      plan: 50, hour: 50, real: 50, shift: 50 }
-		];
-
-		// 数据卡
+		// 工位表由 mesLineProcess/listData(XT,OP060–OP100)拉取,不使用假数据
+		mockData.opTable = [];
+
+		// 数据卡(总览固定展示用)
 		mockData.cards = {
 			productionPeople: 48,
 			deviceCount: 56,
@@ -521,15 +502,15 @@
 	}
 	function drawYAxis(ctx, padL, padT, cw, ch, maxV, minV) {
 		minV = minV || 0;
-		ctx.fillStyle = 'rgba(77,201,245,0.8)';
-		ctx.font = '11px Microsoft YaHei';
+		ctx.fillStyle = 'rgba(77,201,245,0.9)';
+		ctx.font = '13px Microsoft YaHei';
 		ctx.textAlign = 'right';
-		ctx.strokeStyle = 'rgba(0,143,253,0.18)';
+		ctx.strokeStyle = 'rgba(0,143,253,0.22)';
 		ctx.lineWidth = 1;
 		for (var i = 0; i <= 4; i++) {
 			var val = Math.round(minV + (maxV - minV) * (4 - i) / 4);
 			var y = padT + ch * i / 4;
-			ctx.fillText(val, padL - 5, y + 4);
+			ctx.fillText(val, padL - 6, y + 4);
 			ctx.beginPath();
 			ctx.moveTo(padL, y);
 			ctx.lineTo(padL + cw, y);
@@ -541,12 +522,12 @@
 	function drawDoubleBar(id, a, b, labels, maxV) {
 		var p = prepareCanvas(id); if (!p) return;
 		var ctx = p.ctx, w = p.w, h = p.h;
-		var padL = 38, padR = 10, padT = 12, padB = 22;
+		var padL = 46, padR = 12, padT = 16, padB = 28;
 		var cw = w - padL - padR, ch = h - padT - padB;
 		ctx.clearRect(0, 0, w, h);
 		drawYAxis(ctx, padL, padT, cw, ch, maxV);
 		if (!a || a.length < 1) return;
-		var gw = cw / a.length, bw = gw * 0.3;
+		var gw = cw / a.length, bw = gw * 0.28;
 		for (var i = 0; i < a.length; i++) {
 			var cx = padL + i * gw + gw / 2;
 			var h1 = (a[i] / maxV) * ch;
@@ -559,11 +540,11 @@
 			g2.addColorStop(0, '#00C8FF'); g2.addColorStop(1, 'rgba(0,200,255,0.15)');
 			ctx.fillStyle = g2;
 			ctx.fillRect(cx + 2, padT + ch - h2, bw, h2);
-			ctx.fillStyle = '#00FF88'; ctx.font = 'bold 10px Microsoft YaHei';
+			ctx.fillStyle = '#00FF88'; ctx.font = 'bold 12px Microsoft YaHei';
 			ctx.textAlign = 'center'; ctx.fillText(a[i], cx - bw / 2 - 2, padT + ch - h1 - 4);
 			ctx.fillStyle = '#00C8FF'; ctx.fillText(b[i], cx + bw / 2 + 2, padT + ch - h2 - 4);
-			ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = '11px Microsoft YaHei';
-			ctx.fillText(labels[i], cx, h - 6);
+			ctx.fillStyle = 'rgba(77,201,245,0.95)'; ctx.font = '13px Microsoft YaHei';
+			ctx.fillText(labels[i], cx, h - 8);
 		}
 	}
 
@@ -699,7 +680,7 @@
 	function drawAreaLine(id, up, down, labels, maxV) {
 		var p = prepareCanvas(id); if (!p) return;
 		var ctx = p.ctx, w = p.w, h = p.h;
-		var padL = 30, padR = 12, padT = 12, padB = 22;
+		var padL = 42, padR = 14, padT = 16, padB = 28;
 		var cw = w - padL - padR, ch = h - padT - padB;
 		ctx.clearRect(0, 0, w, h);
 		drawYAxis(ctx, padL, padT, cw, ch, maxV);
@@ -735,15 +716,15 @@
 					ctx.bezierCurveTo(cpx2, py2, cpx2, y2, x2, y2);
 				}
 			}
-			ctx.strokeStyle = color; ctx.lineWidth = 1.6; ctx.stroke();
+			ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke();
 		}
 		draw(up, '#00FF88');
 		draw(down, '#00C8FF');
-		ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = '10px Microsoft YaHei'; ctx.textAlign = 'center';
+		ctx.fillStyle = 'rgba(77,201,245,0.95)'; ctx.font = '12px Microsoft YaHei'; ctx.textAlign = 'center';
 		for (var i = 0; i < labels.length; i++) {
 			if (i % 2 === 0) {
 				var x = padL + i * stepX;
-				ctx.fillText(labels[i], x, h - 6);
+				ctx.fillText(labels[i], x, h - 8);
 			}
 		}
 	}
@@ -833,8 +814,6 @@
 			html += '<tr>';
 			html += '<td class="op-col">' + r.code + '</td>';
 			html += '<td class="op-name">' + r.name + '</td>';
-			html += '<td class="op-val">' + r.plan + '</td>';
-			html += '<td class="op-val">' + r.hour + '</td>';
 			html += '<td class="op-val">' + r.real + '</td>';
 			html += '<td class="op-val">' + r.shift + '</td>';
 			html += '</tr>';
@@ -843,42 +822,24 @@
 		if (el) el.innerHTML = html;
 	}
 
-	function applyLatestPlanNumsToTable(planNums) {
-		var list = mockData.opTable || [];
-		for (var i = 0; i < list.length; i++) {
-			list[i].plan = planNums;
-		}
-		renderTable();
+	function parseOprnoNum(code) {
+		var m = String(code || '').toUpperCase().match(/^OP(\d+)/);
+		return m ? parseInt(m[1], 10) : -1;
+	}
+
+	function isOprnoInRange(code, fromNum, toNum) {
+		var n = parseOprnoNum(code);
+		return n >= fromNum && n <= toNum;
 	}
 
+	function applyLatestPlanNumsToTable(planNums) {}
 	function calcRatePercent(actual, plan) {
 		actual = Number(actual) || 0;
 		plan = Number(plan) || 0;
-		if (plan <= 0) {
-			return 0;
-		}
+		if (plan <= 0) return 0;
 		return Math.round(actual * 100 / plan);
 	}
-
-	function syncAchieveRateWithPlan() {
-		mockData.cards = mockData.cards || {};
-		mockData.achieveRate = mockData.achieveRate || { labels: [], actual: [] };
-		var planNums = Number(mockData.cards.planProduction) || 0;
-		var actualProduction = Number(mockData.cards.actualProduction) || 0;
-		var labels = mockData.achieveRate.labels || [];
-		var actualSeries = mockData.achieveRate.actual || [];
-		var targetSeries = [];
-		for (var i = 0; i < labels.length; i++) {
-			targetSeries.push(planNums);
-		}
-		var maxVal = Math.max.apply(null, targetSeries.concat(actualSeries).concat([10]));
-		mockData.achieveRate.target = targetSeries;
-		mockData.achieveRate.max = maxVal <= 0 ? 10 : Math.ceil(maxVal * 1.1);
-		mockData.achieveRate.min = 0;
-		mockData.rate = calcRatePercent(actualProduction, planNums);
-		renderCards();
-		drawAllCharts();
-	}
+	function syncAchieveRateWithPlan() {}
 
 	function updateLineProcessTableByApi() {
 		$.ajax({
@@ -888,7 +849,7 @@
 			data: {
 				pageNo: 1,
 				pageSize: 5000,
-				lineSn: 'MP'
+				lineSn: LINE_SN
 			},
 			success: function(resp) {
 				var rows = extractUserList(resp);
@@ -896,19 +857,21 @@
 				for (var i = 0; i < rows.length; i++) {
 					var p = rows[i] || {};
 					var ls = String(p.lineSn || p.line_sn || '').toUpperCase();
-					if (ls !== 'MP') continue;
+					if (ls !== LINE_SN) continue;
+					var code = p.oprno || '';
+					if (!isOprnoInRange(code, 60, 100)) continue;
 					out.push({
-						code: p.oprno || '',
+						code: code,
 						name: p.title || '',
-						plan: (mockData.cards && mockData.cards.planProduction) || 0,
-						hour: 0,
 						real: 0,
-						shift: 0
+						shift: 0,
+						_opNum: parseOprnoNum(code)
 					});
 				}
+				out.sort(function(a, b) { return a._opNum - b._opNum; });
+				mockData.opTable = out;
+				renderTable();
 				if (out.length > 0) {
-					mockData.opTable = out;
-					renderTable();
 					updateHourlyCapacityByApi();
 				}
 			}
@@ -917,77 +880,58 @@
 
 	function updateHourlyCapacityByApi() {
 		var list = mockData.opTable || [];
-		if (!list.length) {
-			return;
-		}
+		if (!list.length) return;
 		var oprnos = [];
 		for (var i = 0; i < list.length; i++) {
-			if (list[i] && list[i].code) {
-				oprnos.push(list[i].code);
-			}
+			if (list[i] && list[i].code) oprnos.push(list[i].code);
 		}
-		if (!oprnos.length) {
-			return;
-		}
-		fetchCapacityInto('hourlyCapacityData', 'MP', oprnos, 'hour');
-		fetchCapacityInto('dayCapacityData', 'MP', oprnos, 'real');
-		fetchCapacityInto('shiftCapacityData', 'MP', oprnos, 'shift');
+		if (!oprnos.length) return;
+		fetchCapacityInto('dayCapacityData', LINE_SN, oprnos, 'real', {});
+		fetchCapacityInto('shiftCapacityData', LINE_SN, oprnos, 'shift', { fixedTwoShift: 'true' });
 	}
 
-	// 通用:拉取产能数据并写入表格指定字段(hour=小时产能, real=实际/当天产能, shift=当班产能)
-	function fetchCapacityInto(action, lineSn, oprnos, field) {
+	function fetchCapacityInto(action, lineSn, oprnos, field, extraData) {
+		var data = { lineSn: lineSn, oprnos: oprnos.join(',') };
+		if (extraData) {
+			for (var k in extraData) {
+				if (extraData.hasOwnProperty(k)) data[k] = extraData[k];
+			}
+		}
 		$.ajax({
 			url: '${ctx}/mes/mesProductRecord/' + action,
 			type: 'POST',
 			dataType: 'json',
-			data: {
-				lineSn: lineSn,
-				oprnos: oprnos.join(',')
-			},
+			data: data,
 			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
+				if (!resp || resp.result !== 'true' || !resp.data) return;
 				var map = resp.data || {};
 				var list = mockData.opTable || [];
 				for (var i = 0; i < list.length; i++) {
 					var code = list[i].code || '';
-					list[i][field] = Number(map[code]) || 0;
+					var formatted = code.length === 6 ? code.substring(0, 5) : code;
+					list[i][field] = Number(map[code] != null ? map[code] : map[formatted]) || 0;
 				}
 				renderTable();
 			}
 		});
 	}
 
-	function updateLatestPlanProductionByApi() {
-		$.ajax({
-			url: '${ctx}/mes/mesPlan/latestPlanNums',
-			type: 'POST',
-			dataType: 'json',
-			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
-				var planNums = Number(resp.data.nums) || 0;
-				mockData.cards = mockData.cards || {};
-				mockData.cards.planProduction = planNums;
-				applyLatestPlanNumsToTable(planNums);
-				syncAchieveRateWithPlan();
-			}
-		});
-	}
+	function updateLatestPlanProductionByApi() {}
 
-	// 近七日产品质检数据分析(产出=半成品入库,不良=返工记录),按产线 MP 统计
+	// 近七日产品质检:产出=OP100 OK,不良=返工 OP070–OP100
 	function updateWeekQualityByApi() {
 		$.ajax({
 			url: '${ctx}/mes/mesProductRecord/weekQualityData',
 			type: 'POST',
 			dataType: 'json',
-			data: { lineSn: 'MP' },
+			data: {
+				lineSn: LINE_SN,
+				producedOprno: 'OP100',
+				defectOprnoFrom: 'OP070',
+				defectOprnoTo: 'OP100'
+			},
 			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
+				if (!resp || resp.result !== 'true' || !resp.data) return;
 				var d = resp.data;
 				var produced = d.produced || [];
 				var defect = d.defect || [];
@@ -1002,68 +946,16 @@
 					labels: d.labels || [],
 					produced: produced,
 					defect: defect,
-					max: Math.ceil(maxV * 1.2)
-				};
-				// 近七日产量分析(折线图):产量取每日入库数量(半成品入库管理)
-				var pMax = 0, pMin = Number.MAX_VALUE;
-				for (var k = 0; k < produced.length; k++) {
-					var v = Number(produced[k]) || 0;
-					if (v > pMax) pMax = v;
-					if (v < pMin) pMin = v;
-				}
-				if (pMin === Number.MAX_VALUE) { pMin = 0; }
-				var pad = Math.max(1, Math.round((pMax - pMin) * 0.2));
-				mockData.productTrend = {
-					labels: d.labels || [],
-					data: produced,
-					max: pMax + pad,
-					min: Math.max(0, pMin - pad)
+					max: Math.ceil(maxV * 1.15) || 10
 				};
 				drawAllCharts();
 			}
 		});
 	}
 
-	// 各月产量统计图(半成品入库管理按月统计),按产线 MP
-	function updateMonthlyProductByApi() {
-		$.ajax({
-			url: '${ctx}/mes/mesProductRecord/monthlyProductData',
-			type: 'POST',
-			dataType: 'json',
-			data: { lineSn: 'MP' },
-			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
-				var d = resp.data;
-				var data = d.data || [];
-				var maxV = 10;
-				for (var i = 0; i < data.length; i++) {
-					if (Number(data[i]) > maxV) maxV = Number(data[i]);
-				}
-				mockData.monthly = {
-					labels: d.labels || [],
-					data: data,
-					max: Math.ceil(maxV * 1.2)
-				};
-				drawAllCharts();
-			}
-		});
-	}
-	function renderCards() {
-		var c = mockData.cards || {};
-		var m = {
-			productionPeople: c.productionPeople,
-			deviceCount: c.deviceCount,
-			planProduction: c.planProduction,
-			actualProduction: c.actualProduction,
-			gaugeVal: mockData.rate
-		};
-		for (var k in m) {
-			var el = document.getElementById(k);
-			if (el && m[k] != null) el.textContent = m[k];
-		}
-	}
+	function updateMonthlyProductByApi() {}
+
+	function renderCards() {}
 
 	function extractUserList(resp) {
 		if (!resp) return [];
@@ -1231,9 +1123,9 @@
 				data: {
 					pageNo: pageNo,
 					pageSize: pageSize,
-					oprno: 'OP190',
+					oprno: 'OP100',
 					content: 'OK',
-					lineSn: 'MP',
+					lineSn: LINE_SN,
 					updateDate_gte: dayRange.gte,
 					updateDate_lte: dayRange.lte
 				},
@@ -1241,7 +1133,7 @@
 					var list = extractUserList(resp);
 					for (var i = 0; i < list.length; i++) {
 						var r = list[i] || {};
-						if (isOpr190Family(r.oprno) && isRecordContentOk(r.content) && isRecordWithinToday(r, dayRange) && String(r.lineSn || r.line_sn || '').toUpperCase() === 'MP') {
+						if (isOprnoInRange(r.oprno, 100, 100) && isRecordContentOk(r.content) && isRecordWithinToday(r, dayRange) && String(r.lineSn || r.line_sn || '').toUpperCase() === LINE_SN) {
 							totalOk++;
 						}
 					}
@@ -1263,31 +1155,18 @@
 	function drawAllCharts() {
 		var q = mockData.qualityInspect;
 		if (q) drawDoubleBar('chartQuality', q.produced, q.defect, q.labels, q.max);
-		var t = mockData.productTrend;
-		if (t) drawLineChart('chartProduct', t.data, t.labels, t.max, t.min, '#00C8FF');
-		var a = mockData.achieveRate;
-		if (a) drawDualLine('chartAchieve', a.target, a.actual, a.labels, a.max, a.min);
 		var hr = mockData.hourly;
 		if (hr) drawAreaLine('chartHourly', hr.up, hr.down, hr.labels, hr.max);
-		var mo = mockData.monthly;
-		if (mo) drawMonthBar('chartMonth', mo.data, mo.labels, mo.max);
-		drawGauge('gaugeRate', mockData.rate || 0);
 	}
 
 	function refreshData() {
 		generateMockData();
 		renderAlerts();
 		renderTable();
-		renderCards();
-		updateLatestPlanProductionByApi();
 		updateLineProcessTableByApi();
-		updateProductionPeopleByApi();
-		updateDeviceCountByApi();
-		updateActualProductionByApi();
 		updateDeviceAlarmAlertsByApi();
 		updateHourlyUpDownByApi();
 		updateWeekQualityByApi();
-		updateMonthlyProductByApi();
 		drawAllCharts();
 	}