wangxichen 1 день назад
Родитель
Сommit
0164696e4b

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

@@ -7,6 +7,7 @@ import com.jeesite.modules.mes.entity.MesProductRecord;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 生产记录DAO接口
@@ -26,4 +27,14 @@ public interface MesProductRecordDao extends CrudDao<MesProductRecord> {
 
     List<MesProductRecord> selectByOprno(MesProductRecord mesProductRecord);
 
+    /**
+     * 计件统计:按工位+产品前缀+班次+结果分组
+     */
+    List<Map<String, Object>> pieceworkStat(@Param("params") Map<String, Object> params);
+
+    /**
+     * 按小时统计今日产量(用于趋势图)
+     */
+    List<Map<String, Object>> pieceworkHourlyStat(@Param("params") Map<String, Object> params);
+
 }

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

@@ -1208,4 +1208,18 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 	public void updateResultByCheckFirst(MesProductRecord mesProductRecord){
 		mesProductRecordDao.updateResultByCheckFirst(mesProductRecord);
 	}
+
+	/**
+	 * 计件统计查询
+	 */
+	public List<Map<String, Object>> pieceworkStat(Map<String, Object> params){
+		return mesProductRecordDao.pieceworkStat(params);
+	}
+
+	/**
+	 * 按小时统计今日产量(趋势图)
+	 */
+	public List<Map<String, Object>> pieceworkHourlyStat(Map<String, Object> params){
+		return mesProductRecordDao.pieceworkHourlyStat(params);
+	}
 }

+ 165 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductController.java

@@ -290,6 +290,171 @@ public class MesProductController extends BaseController {
 		return "modules/mes/mesScreen2";
 	}
 
+	/**
+	 * 大屏:按小时统计今日 KB24/KA99 产量(用于趋势图)
+	 */
+	@RequestMapping(value = "screenHourly")
+	@ResponseBody
+	public Object screenHourly() {
+		LocalDate today = LocalDate.now();
+		String sdate = today.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + " 00:00:00";
+		String edate = today.plusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + " 00:00:00";
+
+		String prefixConf = Global.getConfig("mes.piecework.product.prefixes");
+		List<String> prefixes = new java.util.ArrayList<>();
+		if (!StringUtils.isBlank(prefixConf)) {
+			for (String p : prefixConf.split(",")) {
+				String t = p.trim();
+				if (!t.isEmpty()) prefixes.add(t);
+			}
+		}
+
+		Map<String, Object> params = new HashMap<>();
+		params.put("dateStart", sdate);
+		params.put("dateEnd",   edate);
+		params.put("prefixes",  prefixes);
+
+		List<Map<String, Object>> hourlyList = mesProductRecordService.pieceworkHourlyStat(params);
+
+		Map<Integer, Map<String, Integer>> hourMap = new LinkedHashMap<>();
+		for (int h = 0; h < 24; h++) {
+			Map<String, Integer> pm = new HashMap<>();
+			for (String p : prefixes) pm.put(p, 0);
+			hourMap.put(h, pm);
+		}
+		for (Map<String, Object> r : hourlyList) {
+			int hour   = ((Number) r.get("hour")).intValue();
+			String pfx = (String) r.get("productPrefix");
+			int cnt    = ((Number) r.get("cnt")).intValue();
+			if (hourMap.containsKey(hour)) {
+				hourMap.get(hour).merge(pfx, cnt, Integer::sum);
+			}
+		}
+
+		List<Map<String, Object>> result = new java.util.ArrayList<>();
+		for (Map.Entry<Integer, Map<String, Integer>> e : hourMap.entrySet()) {
+			Map<String, Object> row = new HashMap<>();
+			row.put("hour",      e.getKey());
+			row.put("prefixMap", e.getValue());
+			result.add(row);
+		}
+
+		Map<String, Object> resp = new HashMap<>();
+		resp.put("result",   1);
+		resp.put("hourly",   result);
+		resp.put("prefixes", prefixes);
+		return resp;
+	}
+
+	/**
+	 * 大屏:计件明细数据(无权限注解,供大屏直接调用)
+	 */
+	@RequestMapping(value = "screenPieceData")
+	@ResponseBody
+	public Object screenPieceData() {
+		// 生产日按 08:30 分界:当前早于 08:30 归入前一生产日
+		// 窗口 [prodDay 08:30, prodDay+1 08:30):白班(08:30~20:30) + 当晚夜班(20:30~次日08:30)
+		java.time.LocalTime nowTime = java.time.LocalTime.now();
+		LocalDate prodDay = nowTime.isBefore(java.time.LocalTime.of(8, 30))
+				? LocalDate.now().minusDays(1) : LocalDate.now();
+		DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+		String dateStart = prodDay.format(fmt) + " 08:30:00";
+		String dateEnd   = prodDay.plusDays(1).format(fmt) + " 08:30:00";
+
+		String prefixConf = Global.getConfig("mes.piecework.product.prefixes");
+		List<String> prefixes = new java.util.ArrayList<>();
+		if (!StringUtils.isBlank(prefixConf)) {
+			for (String p : prefixConf.split(",")) {
+				String t = p.trim();
+				if (!t.isEmpty()) prefixes.add(t);
+			}
+		}
+
+		Map<String, Object> params = new HashMap<>();
+		params.put("dateStart", dateStart);
+		params.put("dateEnd",   dateEnd);
+		params.put("prefixes",  prefixes);
+
+		List<Map<String, Object>> rawList = mesProductRecordService.pieceworkStat(params);
+
+		MesLineProcess lpQuery = new MesLineProcess();
+		lpQuery.setLineSn("XT");
+		lpQuery.setStatus("0");
+		List<MesLineProcess> allProcess = mesLineProcessService.findListOrder(lpQuery);
+
+		// oprno -> prefix -> [白班OK, 白班NG, 夜班OK, 夜班NG]
+		Map<String, Map<String, int[]>> cntMap = new HashMap<>();
+		for (Map<String, Object> r : rawList) {
+			String oprno  = (String) r.get("oprno");
+			String prefix = (String) r.get("productPrefix");
+			String shift  = (String) r.get("shift");
+			String result = (String) r.get("result");
+			int    cnt    = ((Number) r.get("cnt")).intValue();
+			int[] arr = cntMap.computeIfAbsent(oprno, k -> new HashMap<>())
+					.computeIfAbsent(prefix, k -> new int[4]);
+			boolean day = "day".equals(shift);
+			boolean ok  = "OK".equals(result);
+			int idx = (day ? 0 : 2) + (ok ? 0 : 1);
+			arr[idx] += cnt;
+		}
+
+		Map<String, List<MesLineProcess>> childMap = new LinkedHashMap<>();
+		List<MesLineProcess> rootList = new java.util.ArrayList<>();
+		for (MesLineProcess lp : allProcess) {
+			String pid = lp.getPid();
+			if (StringUtils.isBlank(pid) || "0".equals(pid)) {
+				rootList.add(lp);
+			} else {
+				childMap.computeIfAbsent(pid, k -> new java.util.ArrayList<>()).add(lp);
+			}
+		}
+
+		List<Map<String, Object>> groups = new java.util.ArrayList<>();
+		for (MesLineProcess root : rootList) {
+			List<MesLineProcess> children = childMap.getOrDefault(root.getId(), new java.util.ArrayList<>());
+			List<Map<String, Object>> rows = new java.util.ArrayList<>();
+
+			Map<String, Object> parentRow = new LinkedHashMap<>();
+			parentRow.put("oprno",     root.getOprno());
+			parentRow.put("prefixMap", toPrefixMap(cntMap.get(root.getOprno())));
+			rows.add(parentRow);
+
+			for (MesLineProcess child : children) {
+				Map<String, Object> childRow = new LinkedHashMap<>();
+				childRow.put("oprno",     child.getOprno());
+				childRow.put("prefixMap", toPrefixMap(cntMap.get(child.getOprno())));
+				rows.add(childRow);
+			}
+
+			Map<String, Object> group = new LinkedHashMap<>();
+			group.put("title", root.getTitle());
+			group.put("rows",  rows);
+			groups.add(group);
+		}
+
+		Map<String, Object> resp = new HashMap<>();
+		resp.put("result",   1);
+		resp.put("groups",   groups);
+		resp.put("prefixes", prefixes);
+		return resp;
+	}
+
+	/** 把 prefix->int[4] 转成前端用的 prefix->{dayOk,dayNg,nightOk,nightNg} */
+	private Map<String, Object> toPrefixMap(Map<String, int[]> src) {
+		Map<String, Object> out = new HashMap<>();
+		if (src == null) return out;
+		for (Map.Entry<String, int[]> e : src.entrySet()) {
+			int[] a = e.getValue();
+			Map<String, Integer> m = new HashMap<>();
+			m.put("dayOk",   a[0]);
+			m.put("dayNg",   a[1]);
+			m.put("nightOk", a[2]);
+			m.put("nightNg", a[3]);
+			out.put(e.getKey(), m);
+		}
+		return out;
+	}
+
 	@RequestMapping(value = "alarmData")
 	@ResponseBody
 	public CommonResp alarmData() {

+ 10 - 2
src/main/resources/config/application.yml

@@ -55,8 +55,8 @@ jdbc:
   type: mysql
   driver: com.mysql.cj.jdbc.Driver
 
-#  url: jdbc:mysql://127.0.0.1:3306/mes_cloud_610?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
-  url: jdbc:mysql://192.168.113.99:3306/mes_cloud_610?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
+  url: jdbc:mysql://127.0.0.1:3306/mes_cloud_610?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
+#  url: jdbc:mysql://192.168.113.99:3306/mes_cloud_610?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
 #  username: mes_cloud_610
 #  password: wrBrhmpBDxkATCLa
 #  username: root
@@ -626,6 +626,8 @@ shiro:
     ${adminPath}/mes/mesProductRecord/device = anon
     ${adminPath}/mes/mesProduct/screen = anon
     ${adminPath}/mes/mesProduct/screenData = anon
+    ${adminPath}/mes/mesProduct/screenPieceData = anon
+    ${adminPath}/mes/mesProduct/screenHourly = anon
     ${adminPath}/mes/mesProductCcd/testDate = anon
     ${adminPath}/mes/mesProductCcd/add = anon
     ${adminPath}/mes/mesProductRecord/currentProduct = anon
@@ -989,3 +991,9 @@ msg:
 #======================================#
 #========== Project settings ==========#
 #======================================#
+
+# 计件统计产品前缀配置
+mes:
+  piecework:
+    product:
+      prefixes: +KA64,+KB23

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

@@ -49,4 +49,58 @@
         update mes_product_record set content =#{content}
         where sn = #{sn} and oprno = #{oprno} and line_sn = #{lineSn} and craft = #{craft}
     </update>
+
+    <!--
+        计件统计:按工位+产品前缀+班次+结果分组
+        - 固定 line_sn=XT、craft=100000、content IN (OK,NG)
+        - 班次按 TIME(update_date) 判断:08:30~20:30 为白班,其余为夜班
+    -->
+    <select id="pieceworkStat" resultType="java.util.HashMap">
+        SELECT
+            r.oprno,
+            LEFT(r.sn, 5) AS productPrefix,
+            CASE WHEN TIME(r.update_date) &gt;= '08:30:00' AND TIME(r.update_date) &lt; '20:30:00'
+                 THEN 'day' ELSE 'night' END AS shift,
+            r.content    AS result,
+            COUNT(*)     AS cnt
+        FROM mes_product_record r
+        WHERE r.line_sn = 'XT'
+          AND r.craft   = '100000'
+          AND r.content IN ('OK','NG')
+          AND r.update_date &gt;= #{params.dateStart}
+          AND r.update_date &lt;  #{params.dateEnd}
+          <if test="params.prefixes != null and params.prefixes.size() > 0">
+          AND LEFT(r.sn, 5) IN
+              <foreach collection="params.prefixes" item="p" open="(" separator="," close=")">
+                  #{p}
+              </foreach>
+          </if>
+        GROUP BY r.oprno, LEFT(r.sn, 5),
+            CASE WHEN TIME(r.update_date) &gt;= '08:30:00' AND TIME(r.update_date) &lt; '20:30:00'
+                 THEN 'day' ELSE 'night' END,
+            r.content
+        ORDER BY r.oprno ASC, LEFT(r.sn, 5) ASC
+    </select>
+
+    <!-- 按小时统计今日产量,用于趋势图 -->
+    <select id="pieceworkHourlyStat" resultType="java.util.HashMap">
+        SELECT
+            HOUR(r.update_date) AS hour,
+            LEFT(r.sn, 5)       AS productPrefix,
+            COUNT(*)            AS cnt
+        FROM mes_product_record r
+        WHERE r.line_sn = 'XT'
+          AND r.craft   = '100000'
+          AND r.content = 'OK'
+          AND r.update_date &gt;= #{params.dateStart}
+          AND r.update_date &lt;  #{params.dateEnd}
+          <if test="params.prefixes != null and params.prefixes.size() > 0">
+          AND LEFT(r.sn, 5) IN
+              <foreach collection="params.prefixes" item="p" open="(" separator="," close=")">
+                  #{p}
+              </foreach>
+          </if>
+        GROUP BY HOUR(r.update_date), LEFT(r.sn, 5)
+        ORDER BY HOUR(r.update_date) ASC
+    </select>
 </mapper>

+ 591 - 235
src/main/resources/views/modules/mes/mesScreen2.html

@@ -1,244 +1,600 @@
 <!doctype html>
-<html lang="en">
+<html lang="zh">
 <head>
-	<meta charset="UTF-8">
-	<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
-	<meta http-equiv="X-UA-Compatible" content="ie=edge">
-	<title>MES可视化大屏</title>
-	<link rel="stylesheet" href="${ctxStatic}/bootstrap/css/bootstrap.min.css">
-	<link rel="stylesheet" href="${ctxStatic}/screen/style.css">
-	<style>
-		.oprnobox{
-			position: absolute;
-			/*width: 930px;*/
-			height: 688px;
-			z-index: 100;
-		}
-		.oprnobox.box1{
-			top: 171px;
-			left: 25px;
-			right: 25px;
-		}
-		.oprnobox.box2{
-			top: 171px;
-			right: 25px;
-		}
-		.oprnobox-header{
-			width: 100%;
-			height: 58px;
-			line-height: 58px;
-			overflow: hidden;
-			background-color: rgba(20, 55, 120, 0.8);
-			font-size: 20px;
-			color: #008FFD;
-			font-weight: bold;
-			text-align: center;
-		}
-		.oprnobox-header .oprno-name{
-			width: 25%;
-			float: left;
-		}
-		.oprnobox-header .oprno-plan{
-			width: 15%;
-			float: left;
-		}
-		.oprnobox-header .oprno-actual{
-			width: 15%;
-			float: left;
-		}
-		.oprnobox-header .oprno-ng{
-			width: 15%;
-			float: left;
-		}
-		.oprnobox-header .oprno-rate{
-			width: 15%;
-			float: left;
-		}
-		.oprnobox-header .oprno-quality{
-			width: 15%;
-			float: left;
-		}
-		
-		.oprnobox-list{
-			width: 100%;
-			height: 80px;
-			line-height: 80px;
-			overflow: hidden;
-			font-size: 28px;
-			text-align: center;
-			font-family: FZChaoCuHei-M10S;
-			font-weight: 400;
-			color: #FFFFFF;
-			background-image: url("${ctxStatic}/screen/imgs/rbg.png");
-			background-size: 100% 100%;
-			background-repeat: no-repeat;
-		}
-		.oprnobox-list .oprno-name{
-			width: 25%;
-			height: 80px;
-			float: left;
-		}
-		.oprnobox-list .oprno-plan{
-			width: 15%;
-			height: 80px;
-			float: left;
-		}
-		.oprnobox-list .oprno-actual{
-			width: 15%;
-			height: 80px;
-			float: left;
-		}
-		.oprnobox-list .oprno-ng{
-			width: 15%;
-			height: 80px;
-			float: left;
-		}
-		.oprnobox-list .oprno-rate{
-			width: 15%;
-			height: 80px;
-			float: left;
-		}
-		.oprnobox-list .oprno-quality{
-			width: 15%;
-			height: 80px;
-			float: left;
-		}
-	</style>
+    <meta charset="UTF-8">
+    <title>MES可视化大屏</title>
+    <script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
+    <style>
+        * { margin: 0; padding: 0; box-sizing: border-box; }
+        html, body {
+            width: 100%; height: 100%;
+            background: #060e1f; overflow: hidden;
+        }
+        #screen {
+            position: absolute;
+            top: 0; left: 0;
+            width: 1920px; height: 1080px;
+            transform-origin: top left;
+            background: #0a1628;
+            color: #fff;
+            font-family: 'Microsoft YaHei', sans-serif;
+            overflow: hidden;
+        }
+
+        /* 四角装饰 */
+        .corner { position: absolute; width: 26px; height: 26px; z-index: 999; }
+        .corner-tl { top: 8px; left: 8px;  border-top: 2px solid #00d4ff; border-left:  2px solid #00d4ff; }
+        .corner-tr { top: 8px; right: 8px; border-top: 2px solid #00d4ff; border-right: 2px solid #00d4ff; }
+        .corner-bl { bottom: 8px; left: 8px;  border-bottom: 2px solid #00d4ff; border-left:  2px solid #00d4ff; }
+        .corner-br { bottom: 8px; right: 8px; border-bottom: 2px solid #00d4ff; border-right: 2px solid #00d4ff; }
+
+        /* ===== 顶部标题栏 ===== */
+        .top-bar {
+            position: absolute;
+            top: 0; left: 0; right: 0; height: 76px;
+            background: linear-gradient(180deg, rgba(0,60,120,.6) 0%, transparent 100%);
+            border-bottom: 1px solid rgba(0,212,255,.2);
+        }
+        .top-bar::after {
+            content: '';
+            position: absolute;
+            bottom: 0; left: 8%; right: 8%; height: 1px;
+            background: linear-gradient(90deg, transparent, #00d4ff 30%, #00d4ff 70%, transparent);
+        }
+        .top-date {
+            position: absolute; left: 32px; top: 50%;
+            transform: translateY(-50%);
+            font-size: 17px; color: #00d4ff; letter-spacing: 1px;
+        }
+        .top-title {
+            position: absolute; left: 50%; top: 50%;
+            transform: translate(-50%, -50%);
+            font-size: 38px; font-weight: 700; letter-spacing: 6px;
+            color: #fff; white-space: nowrap;
+        }
+        .top-title::before, .top-title::after {
+            content: '◆';
+            position: absolute; top: 50%; transform: translateY(-50%);
+            color: #00d4ff; font-size: 14px;
+        }
+        .top-title::before { left: -50px; }
+        .top-title::after  { right: -50px; }
+        .top-clock {
+            position: absolute; right: 32px; top: 50%;
+            transform: translateY(-50%);
+            font-size: 30px; color: #00d4ff;
+            font-family: 'Courier New', monospace; letter-spacing: 3px;
+            display: flex; align-items: center; gap: 12px;
+        }
+        .live-dot {
+            width: 10px; height: 10px; border-radius: 50%;
+            background: #00e676;
+            box-shadow: 0 0 10px #00e676;
+            animation: blink 1.2s infinite;
+        }
+        @keyframes blink { 0%,100%{opacity:1} 50%{opacity:.15} }
+
+        /* ===== 主表格区 ===== */
+        .body-area {
+            position: absolute;
+            top: 90px; left: 14px; right: 14px; bottom: 14px;
+        }
+        .panel {
+            background: rgba(13,33,68,.85);
+            border: 1px solid rgba(0,212,255,.18);
+            border-radius: 4px;
+            position: relative;
+            height: 100%;
+            display: flex; flex-direction: column;
+        }
+        .panel::before {
+            content: ''; position: absolute;
+            top: -1px; left: -1px; width: 12px; height: 12px;
+            border-top: 2px solid #00d4ff; border-left: 2px solid #00d4ff;
+        }
+        .panel::after {
+            content: ''; position: absolute;
+            bottom: -1px; right: -1px; width: 12px; height: 12px;
+            border-bottom: 2px solid #00d4ff; border-right: 2px solid #00d4ff;
+        }
+
+        .panel-hd {
+            font-size: 16px; color: #00d4ff;
+            padding: 14px 18px 8px;
+            display: flex; align-items: center; gap: 8px;
+            letter-spacing: 1px;
+            flex-shrink: 0;
+        }
+        .panel-hd::before {
+            content: '';
+            width: 4px; height: 18px;
+            background: #00d4ff; border-radius: 2px;
+            box-shadow: 0 0 6px #00d4ff;
+        }
+        .panel-hd-deco { font-size: 12px; color: #2a4a6a; margin-left: 6px; }
+
+        /* ===== 左右双列 ===== */
+        .table-scroll {
+            flex: 1; overflow: hidden;
+            display: flex;
+            margin-top: 8px;
+        }
+        .table-col {
+            flex: 1; min-width: 0;
+            display: flex; flex-direction: column;
+        }
+        .table-col + .table-col { border-left: 1px solid rgba(0,212,255,.25); }
+
+        /* 产品大标题 */
+        .col-title {
+            background: rgba(95,221,161,.15);
+            color: #5fdda1;
+            font-size: 22px; font-weight: 700;
+            text-align: center; padding: 14px 0;
+            letter-spacing: 4px;
+            border-bottom: 1px solid rgba(95,221,161,.3);
+            text-shadow: 0 0 8px rgba(95,221,161,.5);
+            flex-shrink: 0;
+        }
+        .table-col.col-orange .col-title {
+            background: rgba(255,176,69,.15); color: #ffb045;
+            border-bottom-color: rgba(255,176,69,.3);
+            text-shadow: 0 0 8px rgba(255,176,69,.5);
+        }
+
+        /* ===== 表头行(按列分组)===== */
+        .row-head {
+            display: flex;
+            background: #0c2a5a;
+            color: #00d4ff;
+            border-bottom: 1px solid rgba(0,212,255,.3);
+            letter-spacing: 1px;
+            flex-shrink: 0;
+            height: 64px;
+        }
+        .row-head > div {
+            border-left: 1px solid rgba(0,212,255,.18);
+        }
+        .row-head > div:first-child { border-left: none; }
+
+        /* 单格列:贯穿整个表头高度、上下居中 */
+        .hcell-single {
+            display: flex; align-items: center; justify-content: center;
+            text-align: center;
+            font-size: 15px; font-weight: 600;
+            padding: 0 4px;
+            line-height: 1.2;
+        }
+        /* 班次分组块:上半班次名,下半实际/不良 */
+        .hcell-group {
+            display: flex; flex-direction: column;
+        }
+        .hcell-group .grp-title {
+            height: 34px;
+            display: flex; align-items: center; justify-content: center;
+            font-size: 15px; font-weight: 600;
+            color: #ffd479;
+            border-bottom: 1px solid rgba(0,212,255,.2);
+        }
+        .hcell-group .grp-subs {
+            flex: 1;
+            display: flex;
+        }
+        .hcell-group .grp-subs > div {
+            flex: 1;
+            display: flex; align-items: center; justify-content: center;
+            font-size: 13px; font-weight: 600;
+            color: #7fc8e8;
+        }
+        .hcell-group .grp-subs > div + div {
+            border-left: 1px solid rgba(0,212,255,.12);
+        }
+
+        /* 数据行容器:撑满列剩余高度,行均分 */
+        .body-rows {
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+            overflow: hidden;
+        }
+
+        /* ===== 数据行 ===== */
+        .row {
+            display: flex;
+            align-items: center;
+            flex: 1 1 0;
+            min-height: 0;
+            border-bottom: 1px solid rgba(255,255,255,.05);
+            box-sizing: border-box;
+        }
+        .row.odd  { background: rgba(8,22,50,1); }
+        .row.even { background: rgba(18,38,72,1); }
+        .row:hover { background: rgba(0,212,255,.12) !important; }
+
+        /* 不良预警行:左侧红条 + 淡红底 */
+        .row.warn {
+            background: rgba(255,60,80,.14) !important;
+            box-shadow: inset 4px 0 0 #ff3c50;
+        }
+
+        /* 列宽(8 列)*/
+        .c-name   { width: 168px; flex-shrink: 0; }
+        .c-oprno  { width: 150px; flex-shrink: 0; }
+        .c-num    { flex: 1; min-width: 0; }
+        .c-group  { flex: 2; min-width: 0; }
+        .c-total  { width: 96px; flex-shrink: 0; }
+        .c-ngtot  { width: 80px; flex-shrink: 0; }
+
+        .cell {
+            text-align: center;
+            padding: 0 4px;
+            border-left: 1px solid rgba(255,255,255,.04);
+        }
+
+        /* 工序名称 */
+        .cell-name {
+            text-align: left;
+            font-size: 16px; font-weight: 600;
+            color: #e0e8f0;
+            padding-left: 14px;
+            letter-spacing: 1px;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            border-left: none;
+        }
+
+        /* 工位号列:多个工位横排,放不下自动折行 */
+        .cell-oprno {
+            display: flex; flex-direction: row;
+            flex-wrap: wrap;
+            align-items: center; justify-content: center;
+            gap: 2px 6px;
+            font-family: 'Courier New', monospace;
+            font-size: 13px; color: #9aabbc;
+            max-height: 56px; overflow: hidden;
+            padding: 0 4px;
+        }
+
+        /* 数字单元 */
+        .cell-num {
+            font-family: 'Courier New', monospace;
+            font-size: 30px; font-weight: 700;
+        }
+        .num-ok-kb { color: #5fdda1; }
+        .num-ok-ka { color: #ffb045; }
+        .num-ng    { color: #ff5e6c; }
+        .num-zero  { color: #243042; }
+
+        /* 夜班数字统一淡白蓝(对两种产品都清晰区分)*/
+        .row .c-num.night .cell-num.num-ok-kb,
+        .row .c-num.night .cell-num.num-ok-ka {
+            color: #cfdef5;
+        }
+
+        /* 当前班次:表头加亮,数据格不加底色避免视觉割裂 */
+        /* 非当前班次列轻微弱化(仍清晰可读)*/
+        .row .c-num.inactive { opacity: .8; }
+        /* 表头当前班次分组高亮 */
+        .hcell-group.active .grp-title { color: #00e676; }
+        .hcell-group.inactive { opacity: .85; }
+
+        /* 产能合计大数字 */
+        .cell-total {
+            font-size: 27px; font-weight: 900;
+            font-family: 'Arial Black', 'Microsoft YaHei', sans-serif;
+            color: #5fdda1;
+            letter-spacing: 1px;
+            line-height: 1;
+        }
+        .table-col.col-orange .cell-total {
+            color: #ffb045;
+        }
+        /* 不良合计 */
+        .cell-ngtot {
+            font-size: 24px; font-weight: 900;
+            font-family: 'Arial Black', 'Microsoft YaHei', sans-serif;
+            color: #ff5e6c;
+            line-height: 1;
+        }
+        .cell-ngtot.zero { color: #2a3a4a; }
+
+        /* 补齐空行:保持两列等高 */
+        .row-pad {
+            flex: 1 1 0;
+            min-height: 0;
+            border-bottom: 1px solid rgba(255,255,255,.05);
+        }
+
+        .empty-tip {
+            text-align: center; padding: 80px 0;
+            color: #2a3a4a; font-size: 16px;
+        }
+        .refresh-tip {
+            text-align: center; font-size: 12px;
+            color: #2a3a4a; padding: 8px 0; letter-spacing: 1px;
+            flex-shrink: 0;
+        }
+    </style>
 </head>
-<body id="app">
-<div class="screenbg">
-	<img class="bg1" src="${ctxStatic}/screen/imgs/bg.png" alt="">
-	<img class="bg2" style="display: none;" src="${ctxStatic}/screen/imgs/bg2.png" alt="">
-</div>
-<div class="screenbox1">
-	<div class="screenmain">
-		<div class="screen-title">MES系统生产数据大屏</div>
-
-		<div class="oprnobox box1">
-			<div class="oprnobox-header">
-				<div class="oprno-name">工序名称</div>
-				<div class="oprno-plan">计划投产数</div>
-				<div class="oprno-actual">实际产出数</div>
-				<div class="oprno-ng">不良数</div>
-				<div class="oprno-rate">达成率(%)</div>
-				<div class="oprno-quality">合格率(%)</div>
-			</div>
-
-			<div id="numbox">
-				<!--<div class="oprnobox-list">
-					<div class="oprno-name">CMT</div>
-					<div class="oprno-plan">180</div>
-					<div class="oprno-actual">180</div>
-					<div class="oprno-ng">0</div>
-					<div class="oprno-rate">100%</div>
-					<div class="oprno-quality">100%</div>
-				</div>-->
-			</div>
-
-		</div>
-
-	</div>
+<body>
+
+<div id="screen">
+    <div class="corner corner-tl"></div>
+    <div class="corner corner-tr"></div>
+    <div class="corner corner-bl"></div>
+    <div class="corner corner-br"></div>
+
+    <!-- 顶部 -->
+    <div class="top-bar">
+        <div class="top-date" id="topDate"></div>
+        <div class="top-title">MES系统生产数据大屏</div>
+        <div class="top-clock"><span id="topClock"></span><div class="live-dot"></div></div>
+    </div>
+
+    <!-- 主体 -->
+    <div class="body-area">
+        <div class="panel">
+            <div class="panel-hd">工序计件明细<span class="panel-hd-deco">// // //</span></div>
+            <div class="table-scroll">
+                <!-- 左 KA64 -->
+                <div class="table-col" id="colKb">
+                    <div class="col-title">+KA64</div>
+                    <div class="row-head">
+                        <div class="hcell-single c-name">工序名称</div>
+                        <div class="hcell-single c-oprno">工位号</div>
+                        <div class="hcell-group c-group">
+                            <div class="grp-title">白班</div>
+                            <div class="grp-subs"><div>实际</div><div>不良</div></div>
+                        </div>
+                        <div class="hcell-group c-group">
+                            <div class="grp-title">夜班</div>
+                            <div class="grp-subs"><div>实际</div><div>不良</div></div>
+                        </div>
+                        <div class="hcell-single c-total">产能合计</div>
+                        <div class="hcell-single c-ngtot">不良合计</div>
+                    </div>
+                    <div class="body-rows" id="bodyKb"></div>
+                </div>
+                <!-- 右 KB23 -->
+                <div class="table-col col-orange" id="colKa">
+                    <div class="col-title">+KB23</div>
+                    <div class="row-head">
+                        <div class="hcell-single c-name">工序名称</div>
+                        <div class="hcell-single c-oprno">工位号</div>
+                        <div class="hcell-group c-group">
+                            <div class="grp-title">白班</div>
+                            <div class="grp-subs"><div>实际</div><div>不良</div></div>
+                        </div>
+                        <div class="hcell-group c-group">
+                            <div class="grp-title">夜班</div>
+                            <div class="grp-subs"><div>实际</div><div>不良</div></div>
+                        </div>
+                        <div class="hcell-single c-total">产能合计</div>
+                        <div class="hcell-single c-ngtot">不良合计</div>
+                    </div>
+                    <div class="body-rows" id="bodyKa"></div>
+                </div>
+            </div>
+            <div class="refresh-tip">数据每30秒自动刷新</div>
+        </div>
+    </div>
 </div>
 
-<script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
-<!--<script src="${ctxStatic}/common/vue2.7.14.min.js"></script>-->
 <script>
-	let lists = [];
-	let info = null;
-	let curIdx = 0;
-
-	$(window).resize(function(){
-		location.reload()
-	});
-
-	$(function () {
-		changeZoom();
-
-		getData();
-
-		setInterval(function () {
-			getData();
-		},1000*60*2);
-
-		setInterval(function () {
-			formatData();
-		},1000*10);
-	});
-
-	function getData() {
-		let url = "/js/a/mes/mesProduct/screenData";
-		$.post(url,function (ret) {
-			lists = ret.data;
-			curIdx = 0;
-			formatData();
-		});
-	}
-
-	function formatData(){
-		let str = '';
-		const pageSize = 7; // 每页显示7条
-		const totalCount = Object.keys(lists).length;
-		const totalPages = Math.ceil(totalCount / pageSize);
-		
-		// 计算当前页的起始和结束索引
-		const startIdx = curIdx * pageSize;
-		const endIdx = Math.min(startIdx + pageSize, totalCount);
-		
-		let i = 0;
-		for (let o in lists) {
-			if(i >= startIdx && i < endIdx){
-				let item = lists[o];
-				
-				// 计算不良数
-				let ngCount = (item.dayCountTotal || 0) - (item.dayCountOk || 0);
-				
-				// 计算达成率
-				let achieveRate = '0';
-				if(item.planCount && item.planCount > 0){
-					achieveRate = ((item.dayCountTotal || 0) / item.planCount * 100).toFixed(1);
-				}
-				
-				// 计算合格率
-				let qualityRate = '100';
-				if(item.dayCountTotal && item.dayCountTotal > 0){
-					qualityRate = ((item.dayCountOk || 0) / item.dayCountTotal * 100).toFixed(1);
-				}
-				
-				str += '<div class="oprnobox-list">';
-				str += '<div class="oprno-name">'+(item.title || '')+'</div>';
-				str += '<div class="oprno-plan">'+(item.planCount || 0)+'</div>';
-				str += '<div class="oprno-actual">'+(item.dayCountTotal || 0)+'</div>';
-				str += '<div class="oprno-ng">'+ngCount+'</div>';
-				str += '<div class="oprno-rate">'+achieveRate+'%</div>';
-				str += '<div class="oprno-quality">'+qualityRate+'%</div>';
-				str += '</div>';
-			}
-			i++;
-		}
-		
-		$("#numbox").html(str);
-		
-		// 切换到下一页
-		curIdx++;
-		if(curIdx >= totalPages){
-			curIdx = 0;
-		}
-	}
-
-	function changeZoom() {
-		var wb = $(window).width() / 1920;
-		var hb = $(window).height() / 1080;
-		if (wb >= hb) {
-			$('body').css({
-				'zoom': hb,
-			});
-		} else {
-			$('body').css({
-				'zoom': wb,
-			});
-		}
-	}
+(function(){
+
+    var DEFAULT_PREFIXES = ['+KA64', '+KB23'];
+
+    /* ---- 当前班次判断(白班 08:30-20:30,否则夜班)---- */
+    function currentShift(){
+        var d = new Date();
+        var mins = d.getHours() * 60 + d.getMinutes();
+        return (mins >= 510 && mins < 1230) ? 'day' : 'night';  // 510=08:30, 1230=20:30
+    }
+    /* 给表头标记当前班次 */
+    function markHeaderShift(){
+        var sh = currentShift();
+        $('.hcell-group').each(function(i){
+            // 每列两个分组块:偶数索引白班,奇数夜班
+            var blockShift = (i % 2 === 0) ? 'day' : 'night';
+            $(this).removeClass('active inactive')
+                   .addClass(blockShift === sh ? 'active' : 'inactive');
+        });
+    }
+
+    /* ---- 缩放适配 ---- */
+    function fitScreen(){
+        var sx = window.innerWidth  / 1920;
+        var sy = window.innerHeight / 1080;
+        var s  = Math.min(sx, sy);
+        var el = document.getElementById('screen');
+        el.style.transform = 'scale(' + s + ')';
+        el.style.left = ((window.innerWidth  - 1920 * s) / 2) + 'px';
+        el.style.top  = ((window.innerHeight - 1080 * s) / 2) + 'px';
+    }
+    fitScreen();
+    window.addEventListener('resize', fitScreen);
+
+    /* ---- 时钟 ---- */
+    var DAYS = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
+    function pad(n){ return String(n).padStart(2,'0'); }
+    function updateClock(){
+        var d = new Date();
+        $('#topDate').text(d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())+' '+DAYS[d.getDay()]);
+        $('#topClock').text(pad(d.getHours())+':'+pad(d.getMinutes())+':'+pad(d.getSeconds()));
+    }
+    updateClock();
+    setInterval(updateClock, 1000);
+
+    /* ---- 分页 ---- */
+    var kbPages = [], kaPages = [];
+    var kbIdx = 0, kaIdx = 0;
+    var kbTimer = null, kaTimer = null;
+    var PAGE_INTERVAL       = 5000;
+    var MAX_GROUPS_PER_PAGE = 8;   // 每页最多工序数(行会自适应撑满高度)
+    var MIN_GROUPS_PER_PAGE = 4;
+    var ONE_PAGE_LIMIT      = 8;   // 工序数 <= 此值才一页显示,否则分页
+
+    function loadPiece(){
+        $.get('/js/a/mes/mesProduct/screenPieceData', function(data){
+            if(!data || data.result != 1) { console.warn('piece bad', data); return; }
+            var groups = data.groups || [];
+            var prefixes = (data.prefixes && data.prefixes.length) ? data.prefixes : DEFAULT_PREFIXES;
+            var p0 = prefixes[0], p1 = prefixes[1];
+
+            // 两列分别过滤出有数据的工序
+            var kbGroups = filterGroups(groups, p0);
+            var kaGroups = filterGroups(groups, p1);
+
+            // 统一分页:按两列中较多的行数决定每页行数和页数,保证两列每页行数一致、同步翻页
+            var maxCount = Math.max(kbGroups.length, kaGroups.length);
+            var perPage  = calcPerPage(maxCount);
+            kbPages = paginate(kbGroups, perPage);
+            kaPages = paginate(kaGroups, perPage);
+
+            if(kbIdx >= kbPages.length) kbIdx = 0;
+            if(kaIdx >= kaPages.length) kaIdx = 0;
+
+            $('#bodyKb').html(renderPage(kbPages[kbIdx] || [], p0, 'kb'));
+            $('#bodyKa').html(renderPage(kaPages[kaIdx] || [], p1, 'ka'));
+            padColumns();
+
+            startTimers(p0, p1);
+        }).fail(function(xhr){ console.error('piece fail', xhr.status); });
+    }
+
+    /* 两列行数补齐:给少的一列底部加空行(页内行数一致)*/
+    function padColumns(){
+        var $kb = $('#bodyKb'), $ka = $('#bodyKa');
+        $kb.find('.row-pad').remove();
+        $ka.find('.row-pad').remove();
+        var nKb = $kb.children('.row').length;
+        var nKa = $ka.children('.row').length;
+        var diff = nKb - nKa;
+        var pad = '';
+        if(diff > 0){
+            for(var i=0;i<diff;i++) pad += '<div class="row-pad"></div>';
+            $ka.append(pad);
+        } else if(diff < 0){
+            for(var j=0;j<-diff;j++) pad += '<div class="row-pad"></div>';
+            $kb.append(pad);
+        }
+    }
+
+    function startTimers(p0, p1){
+        if(kbTimer){ clearInterval(kbTimer); kbTimer = null; }
+        if(kaTimer){ clearInterval(kaTimer); kaTimer = null; }
+
+        // 两列页数相同,用一个定时器同步翻页
+        var pageCount = Math.max(kbPages.length, kaPages.length);
+        if(pageCount > 1){
+            kbTimer = setInterval(function(){
+                kbIdx = (kbIdx + 1) % kbPages.length;
+                kaIdx = (kaIdx + 1) % kaPages.length;
+                $('#bodyKb').html(renderPage(kbPages[kbIdx], p0, 'kb'));
+                $('#bodyKa').html(renderPage(kaPages[kaIdx], p1, 'ka'));
+                padColumns();
+            }, PAGE_INTERVAL);
+        }
+    }
+
+    /* 过滤:保留有任意数据的工序 */
+    function filterGroups(groups, prefix){
+        var filtered = [];
+        groups.forEach(function(g){
+            var rows = (g.rows || []).filter(function(r){
+                var m = (r.prefixMap||{})[prefix];
+                if(!m) return false;
+                return (m.dayOk||0)+(m.dayNg||0)+(m.nightOk||0)+(m.nightNg||0) > 0;
+            });
+            if(rows.length){
+                filtered.push({ title: g.title, rows: rows });
+            }
+        });
+        return filtered;
+    }
+
+    /* 根据较多列的工序数算出每页行数 */
+    function calcPerPage(totalGroups){
+        if(totalGroups <= 0) return 1;
+        var pageCount;
+        if(totalGroups <= ONE_PAGE_LIMIT){
+            pageCount = 1;
+        } else {
+            pageCount = Math.ceil(totalGroups / MAX_GROUPS_PER_PAGE);
+        }
+        while(pageCount > 1 && Math.ceil(totalGroups / pageCount) < MIN_GROUPS_PER_PAGE){
+            pageCount--;
+        }
+        return Math.ceil(totalGroups / pageCount);
+    }
+
+    /* 按固定每页行数切页,页数 = ceil(maxCount/perPage),短列后续页为空 */
+    function paginate(filtered, perPage){
+        var pages = [];
+        if(!filtered.length){ pages.push([]); return pages; }
+        for(var i = 0; i < filtered.length; i += perPage){
+            pages.push(filtered.slice(i, i + perPage));
+        }
+        return pages;
+    }
+
+    /* 渲染:每个工序一行,工位号横排,白班/夜班实际+不良,产能合计、不良合计 */
+    function renderPage(pageGroups, prefix, side){
+        if(!pageGroups || !pageGroups.length){
+            return '<div class="empty-tip">暂无数据</div>';
+        }
+        var okCls = (side === 'kb') ? 'num-ok-kb' : 'num-ok-ka';
+        var sh = currentShift();
+        var dayCls   = (sh === 'day')   ? 'active' : 'inactive';
+        var nightCls = (sh === 'night') ? 'active' : 'inactive';
+        var html = '';
+        pageGroups.forEach(function(g, gi){
+            var rows = g.rows;
+            var dayOk=0, dayNg=0, nightOk=0, nightNg=0;
+            var oprHtml = '';
+            rows.forEach(function(r){
+                var m = (r.prefixMap||{})[prefix] || {};
+                dayOk   += (m.dayOk||0);
+                dayNg   += (m.dayNg||0);
+                nightOk += (m.nightOk||0);
+                nightNg += (m.nightNg||0);
+                oprHtml += '<span>' + esc(r.oprno||'') + '</span>';
+            });
+            var capTotal = dayOk + nightOk;   // 产能合计 = 白班实际 + 夜班实际
+            var ngTotal  = dayNg + nightNg;   // 不良合计
+            var rowCls = (gi % 2 === 0) ? 'odd' : 'even';
+            if(ngTotal > 0) rowCls += ' warn';
+
+            html += '<div class="row ' + rowCls + '">'
+                  + '<div class="cell-name c-name">' + esc(g.title||'') + '</div>'
+                  + '<div class="cell-oprno c-oprno">' + oprHtml + '</div>'
+                  + '<div class="cell c-num ' + dayCls + '"><span class="cell-num ' + numColor(dayOk, okCls) + '">' + dayOk + '</span></div>'
+                  + '<div class="cell c-num ' + dayCls + '"><span class="cell-num ' + numColor(dayNg, 'num-ng') + '">' + dayNg + '</span></div>'
+                  + '<div class="cell c-num night ' + nightCls + '"><span class="cell-num ' + numColor(nightOk, okCls) + '">' + nightOk + '</span></div>'
+                  + '<div class="cell c-num night ' + nightCls + '"><span class="cell-num ' + numColor(nightNg, 'num-ng') + '">' + nightNg + '</span></div>'
+                  + '<div class="cell c-total cell-total">' + capTotal + '</div>'
+                  + '<div class="cell c-ngtot cell-ngtot' + (ngTotal>0?'':' zero') + '">' + ngTotal + '</div>'
+                  + '</div>';
+        });
+        return html;
+    }
+
+    function numColor(val, cls){
+        return val > 0 ? cls : 'num-zero';
+    }
+
+    function esc(s){
+        return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+    }
+
+    loadPiece();
+    setInterval(loadPiece, 30000);
+    markHeaderShift();
+    setInterval(markHeaderShift, 60000);  // 每分钟检查班次切换
 
+})();
 </script>
 </body>
-</html>
+</html>