hfy před 4 měsíci
rodič
revize
32f5dc56e2

+ 1 - 1
src/main/java/com/jeesite/modules/mes/entity/MesDevice.java

@@ -78,7 +78,7 @@ public class MesDevice extends TreeEntity<MesDevice> {
 	@ExcelFields({
 			@ExcelField(title="设备名称", attrName="treeName", align= ExcelField.Align.CENTER, sort=20),
 			@ExcelField(title="唯一编号", attrName="sn", align= ExcelField.Align.CENTER, sort=30),
-			@ExcelField(title="设备识别码", attrName="sn", align= ExcelField.Align.CENTER, sort=40),
+			@ExcelField(title="设备识别码", attrName="deviceCode", align= ExcelField.Align.CENTER, sort=40),
 			@ExcelField(title="分类", attrName="cateName", align= ExcelField.Align.CENTER, sort=50),
 			@ExcelField(title="IP地址", attrName="ip", align= ExcelField.Align.CENTER, sort=60),
 			@ExcelField(title="产线编号", attrName="lineSn", align= ExcelField.Align.CENTER, sort=70),

+ 118 - 7
src/main/java/com/jeesite/modules/mes/web/MesProductController.java

@@ -85,6 +85,8 @@ public class MesProductController extends BaseController {
 	@Autowired
 	private MesRepairService mesRepairService;
 	@Autowired
+	private MesShippRecordService mesShippRecordService;
+	@Autowired
 	private OcrService ocrService;
 
 
@@ -582,6 +584,18 @@ public class MesProductController extends BaseController {
 		}
 		data.put("date", dateList);
 		data.put("output", outputList);
+		data.put("todayOutput", outputList.isEmpty() ? 0L : outputList.get(outputList.size() - 1));
+
+		Calendar monthCal = Calendar.getInstance();
+		monthCal.set(Calendar.DAY_OF_MONTH, 1);
+		monthCal.set(Calendar.HOUR_OF_DAY, 0);
+		monthCal.set(Calendar.MINUTE, 0);
+		monthCal.set(Calendar.SECOND, 0);
+		monthCal.set(Calendar.MILLISECOND, 0);
+		MesProduct monthProduct = new MesProduct();
+		monthProduct.getSqlMap().getWhere().and("create_date", QueryType.GTE, monthCal.getTime());
+		monthProduct.getSqlMap().getWhere().and("create_date", QueryType.LT, todayEnd);
+		data.put("monthOutput", mesProductService.findCount(monthProduct));
 
 		List<String> hourTimes = new ArrayList<>();
 		List<Long> perHourData = new ArrayList<>();
@@ -610,6 +624,19 @@ public class MesProductController extends BaseController {
 		data.put("quality1", quality1);
 		data.put("quality2", quality2);
 
+		List<Long> warehouseOutList = new ArrayList<>();
+		for (int i = 6; i >= 0; i--) {
+			Calendar dayCal = (Calendar) calendar.clone();
+			dayCal.add(Calendar.DATE, -i);
+			Date start = dayCal.getTime();
+			dayCal.add(Calendar.DATE, 1);
+			Date end = dayCal.getTime();
+			warehouseOutList.add(sumShippRecordNums(start, end));
+		}
+		data.put("warehouseOut", warehouseOutList);
+		data.put("todayOut", warehouseOutList.isEmpty() ? 0L : warehouseOutList.get(warehouseOutList.size() - 1));
+		data.put("monthOut", sumShippRecordNums(monthCal.getTime(), todayEnd));
+
 		List<Map<String, Object>> workStationData = new ArrayList<>();
 		MesProcessProduct processProductQuery = new MesProcessProduct();
 		List<MesProcessProduct> processProductList = mesProcessProductService.findList(processProductQuery);
@@ -743,23 +770,59 @@ public class MesProductController extends BaseController {
 		data.put("faultRates", faultRates);
 		data.put("faultDeviceCounts", faultDeviceCounts);
 
-		//根据产线编号查询工位号和产线名称
+		List<Map<String, Object>> ngWarnings = new ArrayList<>();
+		Calendar ngBefore = Calendar.getInstance();
+		ngBefore.add(Calendar.HOUR_OF_DAY, -48);
+		MesProductRecord ngRecordQuery = new MesProductRecord();
+		ngRecordQuery.setContent("NG");
+		ngRecordQuery.setUpdateDate_lte(ngBefore.getTime());
+		ngRecordQuery.getSqlMap().getOrder().setOrderBy("a.update_date ASC");
+		List<MesProductRecord> ngRecordList = mesProductRecordService.findList(ngRecordQuery);
+		if (ngRecordList != null) {
+			SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+			for (MesProductRecord record : ngRecordList) {
+				Map<String, Object> item = MapUtils.newHashMap();
+				item.put("sn", record.getSn());
+				item.put("oprno", record.getOprno());
+				item.put("title", StringUtils.isEmpty(record.getTitle()) ? record.getOprnoTitle() : record.getTitle());
+				item.put("result", record.getContent());
+				item.put("time", record.getUpdateDate() == null ? "" : dateTimeFormat.format(record.getUpdateDate()));
+				ngWarnings.add(item);
+				if (ngWarnings.size() >= 50) {
+					break;
+				}
+			}
+		}
+		data.put("ngWarnings", ngWarnings);
+
+		int dayPlan = parseInt(planCount);
+		SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+		MesScreenPlan currentPlan = mesScreenPlanService.getPlan(dateTimeFormat.format(new Date()));
+
+		// 根据产线编号查询全部正常产线的工位号和产线名称
 		List<List<Object>> oprnoData = new ArrayList<>();
 		MesLineProcess mesLineProcess = new MesLineProcess();
 		mesLineProcess.getSqlMap().getWhere().and("a.pid",QueryType.IS_NULL,"");
-		mesLineProcess.setLineId("1670043381650743296");
 		mesLineProcess.setStatus("0");
-		mesLineProcess.getSqlMap().getOrder().setOrderBy("CAST(SUBSTRING(a.oprno, 3, 3) AS UNSIGNED) ASC, a.oprno ASC");
+		mesLineProcess.getSqlMap().getOrder().setOrderBy("b.sn ASC, CAST(SUBSTRING(a.oprno, 3, 3) AS UNSIGNED) ASC, a.oprno ASC");
 		List<MesLineProcess> processes = mesLineProcessService.findList(mesLineProcess);
 		processes.forEach(x->{
 			List<Object> row = new ArrayList<>();
+			String lineSn = x.getLineSn();
+			String lineName = StringUtils.isEmpty(x.getLineTitle()) ? lineSn : x.getLineTitle();
+			if (StringUtils.isNotEmpty(lineSn) && StringUtils.isNotEmpty(lineName) && !lineName.contains(lineSn)) {
+				lineName = lineName + "(" + lineSn + ")";
+			}
+			row.add(lineName);
 			row.add(x.getOprno());   // 工位号
 			row.add(x.getTitle());   // 工位名称
 			MesProductRecord recorda=new MesProductRecord();
 			recorda.setOprno(x.getOprno());
 			recorda.setCraft("100000");
 			recorda.setContent("OK");
-			recorda.setLineSn("HEVXT");
+			if (StringUtils.isNotEmpty(lineSn)) {
+				recorda.setLineSn(lineSn);
+			}
 			//获取每个工位当天实际产能
 			Calendar start = Calendar.getInstance();
 			start.set(Calendar.HOUR_OF_DAY, 0);
@@ -781,15 +844,19 @@ public class MesProductController extends BaseController {
 			record12h.setOprno(x.getOprno());
 			record12h.setCraft("100000");
 			record12h.setContent("OK");
-			recorda.setLineSn("HEVXT");
+			if (StringUtils.isNotEmpty(lineSn)) {
+				record12h.setLineSn(lineSn);
+			}
 			Calendar before12h = Calendar.getInstance();
 			before12h.add(Calendar.HOUR_OF_DAY, -12);
 
 			record12h.setUpdateDate_gte(before12h.getTime());
 
 			long count12h = mesProductRecordService.findCount(record12h);
-			row.add("50");
-			row.add("4");
+			int planCapacity = resolveLiuanPlanCapacity(x.getOprno(), currentPlan, dayPlan);
+			int hourCapacity = planCapacity > 0 ? (int)Math.ceil(planCapacity / 12.0) : 0;
+			row.add(planCapacity);
+			row.add(hourCapacity);
 			row.add(count);
 			row.add(count12h);
 
@@ -803,6 +870,50 @@ public class MesProductController extends BaseController {
 		return resp;
 	}
 
+	private long sumShippRecordNums(Date start, Date end) {
+		MesShippRecord query = new MesShippRecord();
+		query.setCreateDate_gte(start);
+		query.setCreateDate_lte(end);
+		List<MesShippRecord> list = mesShippRecordService.findList(query);
+		long total = 0L;
+		if (list != null) {
+			for (MesShippRecord item : list) {
+				total += item.getNums() == null ? 0L : item.getNums();
+			}
+		}
+		return total;
+	}
+
+	private int parseInt(String value) {
+		try {
+			return StringUtils.isEmpty(value) ? 0 : Integer.parseInt(value);
+		} catch (NumberFormatException ex) {
+			return 0;
+		}
+	}
+
+	private int resolveLiuanPlanCapacity(String oprno, MesScreenPlan plan, int defaultPlan) {
+		if (plan == null || StringUtils.isEmpty(oprno)) {
+			return defaultPlan;
+		}
+		int oprnoNum = parseOprnoNum(oprno);
+		if (oprnoNum > 0 && oprnoNum <= 210 && plan.getP1xx() != null) {
+			return plan.getP1xx();
+		}
+		if (oprnoNum > 210 && plan.getP2xx() != null) {
+			return plan.getP2xx();
+		}
+		return defaultPlan;
+	}
+
+	private int parseOprnoNum(String oprno) {
+		String digits = oprno.replaceAll("\\D", "");
+		if (digits.length() == 0) {
+			return 0;
+		}
+		return parseInt(digits);
+	}
+
 	@RequestMapping(value = "screenDataHourly")
 	@ResponseBody
 	public CommonResp screenDataHourly() {

+ 266 - 207
src/main/resources/views/modules/mes/mesScreenLiuan.html

@@ -214,6 +214,15 @@
         .left-top .chart {
             height: 178px;
         }
+        .left-bottom #warehouseChart {
+            position: absolute;
+            left: 20px;
+            right: 20px;
+            top: 130px;
+            bottom: 18px;
+            width: auto;
+            height: auto;
+        }
         .chart-with-metrics {
             position: absolute;
             left: 20px;
@@ -221,23 +230,6 @@
             top: 126px;
             bottom: 16px;
         }
-        .plan-badge {
-            position: absolute;
-            right: 28px;
-            top: 92px;
-            width: 92px;
-            height: 92px;
-            border-radius: 50%;
-            background: linear-gradient(160deg, #ff4271, #ff2366);
-            display: flex;
-            align-items: center;
-            justify-content: center;
-            color: #fff;
-            font-size: 24px;
-            font-weight: 900;
-            box-shadow: 0 0 18px rgba(255, 48, 105, .7);
-            z-index: 4;
-        }
         .table-title {
             position: absolute;
             left: 0;
@@ -266,7 +258,7 @@
         .jph-table td {
             height: 52px;
             text-align: center;
-            font-size: 20px;
+            font-size: 18px;
             font-weight: 800;
             border-bottom: 1px solid rgba(73, 193, 255, .12);
         }
@@ -279,7 +271,11 @@
         .jph-table td.name {
             text-align: left;
             padding-left: 14px;
-            font-size: 18px;
+            font-size: 16px;
+        }
+        .jph-table td.line {
+            color: #65f2ff;
+            font-size: 16px;
         }
         .ng-table {
             width: 100%;
@@ -305,6 +301,24 @@
         .ng-table tbody tr:nth-child(odd) td {
             background: rgba(10, 88, 153, .36);
         }
+        .jph-table .empty-row td,
+        .ng-table .empty-row td {
+            color: rgba(210, 242, 255, .68);
+            font-size: 16px;
+            font-weight: 600;
+            text-align: center;
+            background: rgba(8, 80, 145, .26);
+        }
+        .ng-table .empty-row td {
+            font-size: 13px;
+        }
+        .jph-table tbody,
+        .ng-table tbody {
+            transition: opacity .28s ease;
+        }
+        .table-switching {
+            opacity: .18;
+        }
         .mini-stat {
             position: absolute;
             left: 24px;
@@ -376,6 +390,11 @@
         }
         .daily-legend .online::before { background: #10f2b4; }
         .daily-legend .offline::before { background: #18b8ff; }
+        @media (max-width: 900px), (max-height: 520px) {
+            body {
+                background: #020b20;
+            }
+        }
     </style>
 </head>
 <body>
@@ -405,9 +424,8 @@
             <div class="panel-title">生产计划交付率</div>
             <div class="mini-stat">
                 <img src="${ctxStatic}/screen/imgs/liuan10.png" alt="">
-                <div><span>生产计划交付率</span><strong id="deliveryRateText">98%</strong></div>
+                <div><span>生产计划交付率</span><strong id="deliveryRateText">0%</strong></div>
             </div>
-            <div class="plan-badge" id="deliveryBadge">98%</div>
             <div class="legend"><span><i class="line-yellow"></i>目标</span><span><i class="line-green"></i>实际</span></div>
             <div class="chart-with-metrics" id="deliveryChart"></div>
         </section>
@@ -428,16 +446,17 @@
         </section>
 
         <section class="panel center-main">
-            <div class="table-title">HEV各工位实际JPH</div>
+            <div class="table-title">各产线工位实际JPH</div>
             <table class="jph-table">
                 <thead>
                 <tr>
-                    <th style="width:110px;">工位</th>
-                    <th style="width:200px;">工位名称</th>
-                    <th style="width:100px;">计划产能</th>
-                    <th style="width:100px;">小时产能</th>
-                    <th style="width:100px;">实际产能</th>
-                    <th style="width:100px;">当班产能(12小时内)</th>
+                    <th style="width:110px;">产线</th>
+                    <th style="width:95px;">工位</th>
+                    <th style="width:190px;">工位名称</th>
+                    <th style="width:95px;">计划产能</th>
+                    <th style="width:95px;">小时产能</th>
+                    <th style="width:95px;">实际产能</th>
+                    <th style="width:120px;">当班产能(12小时内)</th>
                 </tr>
                 </thead>
                 <tbody id="jphTable"></tbody>
@@ -464,9 +483,9 @@
             <div class="panel-title">IQC检测合格率</div>
             <div class="mini-stat">
                 <img src="${ctxStatic}/screen/imgs/liuan13.png" alt="">
-                <div><span>IQC检测合格率</span><strong id="iqcRateText">98%</strong></div>
+                <div><span>IQC检测合格率</span><strong id="iqcRateText">0%</strong></div>
             </div>
-            <div class="rate-number" id="iqcRate">98%</div>
+            <div class="rate-number" id="iqcRate">0%</div>
             <div class="legend"><span><i class="line-yellow"></i>不合格数量</span><span><i class="line-green"></i>合格率</span></div>
             <div class="chart-with-metrics" id="iqcChart"></div>
         </section>
@@ -481,7 +500,7 @@
             <div class="panel-title">生产计划达成率</div>
             <div class="mini-stat">
                 <img src="${ctxStatic}/screen/imgs/liuan10.png" alt="">
-                <div><span>今日达成率</span><strong id="todayPlanRate">90%</strong></div>
+                <div><span>今日达成率</span><strong id="todayPlanRate">0%</strong></div>
             </div>
             <div class="legend"><span><i class="line-yellow"></i>目标</span><span><i class="line-green"></i>实际</span></div>
             <div class="chart-with-metrics" id="planRateChart"></div>
@@ -491,7 +510,7 @@
             <div class="panel-title">生产计划完成率</div>
             <div class="mini-stat">
                 <img src="${ctxStatic}/screen/imgs/liuan13.png" alt="">
-                <div><span>生产计划完成率</span><strong id="finishRate">98%</strong></div>
+                <div><span>生产计划完成率</span><strong id="finishRate">0%</strong></div>
             </div>
             <div class="legend"><span><i class="line-yellow"></i>目标</span><span><i class="line-green"></i>实际</span></div>
             <div class="chart-with-metrics" id="finishRateChart"></div>
@@ -502,57 +521,23 @@
 <script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
 <script src="${ctxStatic}/echarts/4.2/echarts.min.js"></script>
 <script>
-    // screenData 保存 /mes/mesProduct/screenData4 返回的综合大屏数据:
-    // 产量、计划数、近七日数据、质量数据、告警数据、工位数据等都从这里取。
     var screenData = {};
-
-    // hourlyData 保存 /mes/mesProduct/screenDataHourly 返回的小时上下线数据。
-    // 当前接口按产线编码分组,例如 hevxt、b13xt,每组包含 hours、online、offline。
     var hourlyData = {};
-
-    // ECharts 实例缓存。重复刷新数据时复用实例,避免每次 setOption 都重新创建 canvas。
     var chartMap = {};
-    var jphPageTimer = null;
-    var jphPageIndex = 0;
     var jphRows = [];
-    var jphPageSize = 9;
-
-    // 当后台接口暂时没有返回对应数组时,页面用这些兜底值保持版面完整。
-    // 后续如果后台补齐精确接口,只需要保证字段有值,兜底数据会自动失效。
-    var fallbackDates = ['12月1日', '12月2日', '12月3日', '12月4日', '12月5日', '12月6日', '12月7日'];
-
-
-    // 大屏按蓝湖稿 1920*1080 制作;这里根据浏览器窗口等比缩放并居中显示。
-    // Runtime data used by the Liuan screen. Keep these declarations separate from
-    // generated comments so the browser never treats them as commented-out code.
-    var screenData = {};
-    var hourlyData = {};
-    var chartMap = {};
-    var jphPageTimer = null;
+    var ngRows = [];
     var jphPageIndex = 0;
-    var jphRows = [];
+    var ngPageIndex = 0;
     var jphPageSize = 9;
-    var fallbackDates = ['12月1日', '12月2日', '12月3日', '12月4日', '12月5日', '12月6日', '12月7日'];
-    var stationRows = [
-        ['OP10', '上料+打码'],
-        ['OP20', '底板组件-正面FSW'],
-        ['OP25', '底板组件-打磨'],
-        ['OP30', '底板组件-反面FSW'],
-        ['OP40', '底板组件-CNC'],
-        ['OP50', '干冰清洁'],
-        ['OP55', '模组架+封板预装'],
-        ['OP60', '封板正面FSW'],
-        ['OP65', '打磨'],
-        ['OP70', '封板反面FSW']
-    ];
+    var ngPageSize = 6;
+    var jphPageTimer = null;
+    var ngPageTimer = null;
 
     function fitScreen() {
         var scale = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
         var left = (window.innerWidth - 1920 * scale) / 2;
         var top = (window.innerHeight - 1080 * scale) / 2;
-        $('#screen').css({
-            transform: 'translate(' + left + 'px,' + top + 'px) scale(' + scale + ')'
-        });
+        $('#screen').css({ transform: 'translate(' + left + 'px,' + top + 'px) scale(' + scale + ')' });
     }
 
     function pad(n) {
@@ -565,20 +550,61 @@
         $('#clock').text(now.getFullYear() + '年' + pad(now.getMonth() + 1) + '月' + pad(now.getDate()) + '日 ' + weeks[now.getDay()] + '  ' + pad(now.getHours()) + ':' + pad(now.getMinutes()));
     }
 
-    // 接口数据为空时统一回退到 fallback,避免某个图表因为空数组直接不显示。
-    function getArray(value, fallback) {
-        return value && value.length ? value : fallback;
+    function recentDates(days) {
+        var list = [];
+        var now = new Date();
+        for (var i = days - 1; i >= 0; i--) {
+            var d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i);
+            list.push((d.getMonth() + 1) + '月' + d.getDate() + '日');
+        }
+        return list;
+    }
+
+    function getArray(value) {
+        return $.isArray(value) ? value : [];
+    }
+
+    function numberValue(value) {
+        var n = Number(value);
+        return isNaN(n) ? 0 : n;
+    }
+
+    function alignNumbers(list, length) {
+        var source = getArray(list);
+        var result = [];
+        for (var i = 0; i < length; i++) {
+            result.push(numberValue(source[i]));
+        }
+        return result;
     }
 
     function sum(list) {
-        list = list || [];
         var total = 0;
+        list = getArray(list);
         for (var i = 0; i < list.length; i++) {
-            total += Number(list[i]) || 0;
+            total += numberValue(list[i]);
         }
         return total;
     }
 
+    function percent(numerator, denominator) {
+        numerator = numberValue(numerator);
+        denominator = numberValue(denominator);
+        if (denominator <= 0) {
+            return 0;
+        }
+        return Math.min(100, Math.max(0, Math.round(numerator * 100 / denominator)));
+    }
+
+    function escapeHtml(value) {
+        return String(value == null ? '' : value)
+            .replace(/&/g, '&amp;')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;')
+            .replace(/"/g, '&quot;')
+            .replace(/'/g, '&#39;');
+    }
+
     function chart(id, option) {
         var el = document.getElementById(id);
         if (!el) return;
@@ -588,18 +614,10 @@
         chartMap[id].setOption(option, true);
     }
 
-    // 图表通用网格配置,保证不同图表的坐标轴留白一致。
     function baseGrid(bottom) {
-        return {
-            top: 18,
-            left: 35,
-            right: 16,
-            bottom: bottom || 28,
-            containLabel: true
-        };
+        return { top: 18, left: 35, right: 16, bottom: bottom || 28, containLabel: true };
     }
 
-    // 图表通用坐标轴样式,匹配蓝湖稿的蓝色科技风。
     function axisStyle() {
         return {
             axisLine: { lineStyle: { color: 'rgba(97, 199, 255, .35)' } },
@@ -609,10 +627,9 @@
         };
     }
 
-    // 渲染柱状图:用于工件数据统计、出库数据统计等只需要一组数值的面板。
-    function renderBars(id, dates, values, color1, color2) {
+    function renderBars(id, dates, values, color1, color2, grid) {
         chart(id, {
-            grid: baseGrid(22),
+            grid: grid || baseGrid(22),
             tooltip: { trigger: 'axis' },
             xAxis: Object.assign({ type: 'category', data: dates }, axisStyle()),
             yAxis: Object.assign({ type: 'value' }, axisStyle()),
@@ -631,7 +648,6 @@
         });
     }
 
-    // 渲染“实际/目标”双线面积图:用于计划交付率、计划达成率、计划完成率。
     function renderLineArea(id, dates, values, target) {
         chart(id, {
             grid: baseGrid(22),
@@ -659,7 +675,7 @@
                     name: '目标',
                     type: 'line',
                     smooth: true,
-                    data: target || values.map(function () { return 1100; }),
+                    data: target || [],
                     symbol: 'none',
                     lineStyle: { color: '#ffcf30', width: 2 }
                 }
@@ -667,32 +683,41 @@
         });
     }
 
-    // 渲染百分比折线图。当前页面未直接调用,保留给后续接入纯百分比指标时复用。
-    function renderPercent(id, dates, values) {
+    function renderPercent(id, dates, values, targetValue) {
         chart(id, {
             grid: baseGrid(22),
             tooltip: { trigger: 'axis' },
             xAxis: Object.assign({ type: 'category', boundaryGap: false, data: dates }, axisStyle()),
             yAxis: Object.assign({ type: 'value', min: 0, max: 100, axisLabel: { color: '#bdeeff', fontSize: 10, formatter: '{value}%' } }, axisStyle()),
-            series: [{
-                type: 'line',
-                smooth: true,
-                data: values,
-                symbol: 'circle',
-                symbolSize: 6,
-                lineStyle: { color: '#19f0c2', width: 3 },
-                itemStyle: { color: '#19f0c2' },
-                areaStyle: {
-                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                        { offset: 0, color: 'rgba(25, 240, 194, .45)' },
-                        { offset: 1, color: 'rgba(24, 184, 255, .05)' }
-                    ])
+            series: [
+                {
+                    name: '实际',
+                    type: 'line',
+                    smooth: true,
+                    data: values,
+                    symbol: 'circle',
+                    symbolSize: 6,
+                    lineStyle: { color: '#19f0c2', width: 3 },
+                    itemStyle: { color: '#19f0c2' },
+                    areaStyle: {
+                        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                            { offset: 0, color: 'rgba(25, 240, 194, .45)' },
+                            { offset: 1, color: 'rgba(24, 184, 255, .05)' }
+                        ])
+                    }
+                },
+                {
+                    name: '目标',
+                    type: 'line',
+                    smooth: true,
+                    data: dates.map(function () { return targetValue == null ? 100 : targetValue; }),
+                    symbol: 'none',
+                    lineStyle: { color: '#ffcf30', width: 2 }
                 }
-            }]
+            ]
         });
     }
 
-    // 渲染 IQC 合格率:柱子表示不合格数量,折线表示合格率。
     function renderIqc(dates, bad, rates) {
         chart('iqcChart', {
             grid: baseGrid(24),
@@ -729,8 +754,6 @@
         });
     }
 
-    // 渲染每日上下线统计。数据来自 screenDataHourly:
-    // hLine.hours 为横轴小时,hLine.online 为上线数,hLine.offline 为下线数。
     function renderDaily(dates, online, offline) {
         chart('dailyChart', {
             grid: baseGrid(20),
@@ -760,25 +783,39 @@
         });
     }
 
-    // 渲染中间 JPH 表格。rows 优先取 screenData.oprnoData,
-    // 若旧接口没有该字段,则兼容使用 screenData.workStationData。
-    function renderJphTable(rows) {
-        jphRows = [];
-        var sourceRows = rows && rows.length ? rows : [];
-        var maxLength =  sourceRows.length;
+    function calculateTablePages() {
+        jphPageSize = Math.max(6, Math.floor(($('.center-main').height() - 124) / 52));
+        ngPageSize = Math.max(4, Math.floor(($('.right-top').height() - 104) / 30));
+    }
 
-        for (var i = 0; i < maxLength; i++) {
+    function normalizeJphRows(rows) {
+        var sourceRows = getArray(rows);
+        var result = [];
+        for (var i = 0; i < sourceRows.length; i++) {
             var item = sourceRows[i] || {};
-            jphRows.push([
-                item[0],
-                item[1],
-                item[2],
-                item[3],
-                item[4],
-                item[5]
-            ]);
-        }
+            if ($.isArray(item)) {
+                if (item.length >= 7) {
+                    result.push([item[0], item[1], item[2], item[3], item[4], item[5], item[6]]);
+                } else {
+                    result.push(['', item[0], item[1], item[2], item[3], item[4], item[5]]);
+                }
+            } else {
+                result.push([
+                    item.lineTitle || item.lineSn || '',
+                    item.oprno || '',
+                    item.title || item.oprnoTitle || item.sn || '',
+                    item.planCapacity || 0,
+                    item.hourCapacity || 0,
+                    item.actualCapacity || item.dayCount || 0,
+                    item.shiftCapacity || item.shiftCount || 0
+                ]);
+            }
+        }
+        return result;
+    }
 
+    function renderJphTable(rows) {
+        jphRows = normalizeJphRows(rows);
         jphPageIndex = 0;
         renderJphTablePage();
         startJphPageTimer();
@@ -788,16 +825,20 @@
         var html = '';
         var start = jphPageIndex * jphPageSize;
         var pageRows = jphRows.slice(start, start + jphPageSize);
-
+        if (!pageRows.length) {
+            $('#jphTable').html('<tr class="empty-row"><td colspan="7">暂无工位 JPH 数据</td></tr>');
+            return;
+        }
         for (var i = 0; i < pageRows.length; i++) {
             var row = pageRows[i];
             html += '<tr>' +
-                '<td>' + row[0] + '</td>' +
-                '<td class="name">' + row[1] + '</td>' +
-                '<td>' + row[2] + '</td>' +
-                '<td>' + row[3] + '</td>' +
-                '<td>' + row[4] + '</td>' +
-                '<td>' + row[5] + '</td>' +
+                '<td class="line">' + escapeHtml(row[0]) + '</td>' +
+                '<td>' + escapeHtml(row[1]) + '</td>' +
+                '<td class="name">' + escapeHtml(row[2]) + '</td>' +
+                '<td>' + escapeHtml(row[3]) + '</td>' +
+                '<td>' + escapeHtml(row[4]) + '</td>' +
+                '<td>' + escapeHtml(row[5]) + '</td>' +
+                '<td>' + escapeHtml(row[6]) + '</td>' +
                 '</tr>';
         }
         $('#jphTable').html(html);
@@ -808,105 +849,121 @@
             clearInterval(jphPageTimer);
             jphPageTimer = null;
         }
-
         var pageCount = Math.ceil(jphRows.length / jphPageSize);
-        if (pageCount <= 1) {
-            return;
-        }
-
+        if (pageCount <= 1) return;
         jphPageTimer = setInterval(function () {
             jphPageIndex = (jphPageIndex + 1) % pageCount;
-            renderJphTablePage();
-        }, 5000);
+            switchTablePage('#jphTable', renderJphTablePage);
+        }, 4500);
     }
 
-    // 渲染右上 NG 预警表。当前复用 screenData.alarms;
-    // 如果后续后台提供专门的“超48小时未处理 NG 记录”接口,可在 loadData 中替换数据源。
     function renderNgTable(items) {
-        var source = getArray(items, [
-            { sn: '20012549...', oprno: 'OP40', title: '工位结果', result: 'NG', time: '2026-05-29 10:07' },
-            { sn: '20012549...', oprno: 'OP40', title: '工位结果', result: 'NG', time: '2026-05-29 10:07' },
-            { sn: '20012549...', oprno: 'OP40', title: '工位结果', result: 'NG', time: '2026-05-29 10:07' },
-            { sn: '20012549...', oprno: 'OP40', title: '工位结果', result: 'NG', time: '2026-05-29 10:07' },
-            { sn: '20012549...', oprno: 'OP40', title: '工位结果', result: 'NG', time: '2026-05-29 10:07' }
-        ]);
+        ngRows = getArray(items);
+        ngPageIndex = 0;
+        renderNgTablePage();
+        startNgPageTimer();
+    }
+
+    function renderNgTablePage() {
         var html = '';
-        for (var i = 0; i < Math.min(source.length, 5); i++) {
-            var row = source[i];
+        var start = ngPageIndex * ngPageSize;
+        var pageRows = ngRows.slice(start, start + ngPageSize);
+        if (!pageRows.length) {
+            $('#ngTable').html('<tr class="empty-row"><td colspan="5">暂无超 48 小时 NG 预警</td></tr>');
+            return;
+        }
+        for (var i = 0; i < pageRows.length; i++) {
+            var row = pageRows[i] || {};
             html += '<tr>' +
-                '<td>' + (row.sn || row.msg || '') + '</td>' +
-                '<td>' + (row.oprno || 'OP40') + '</td>' +
-                '<td>' + (row.title || '工位结果') + '</td>' +
-                '<td>' + (row.result || 'NG') + '</td>' +
-                '<td>' + (row.time || row.createDate || '') + '</td>' +
+                '<td title="' + escapeHtml(row.sn || row.msg || '') + '">' + escapeHtml(row.sn || row.msg || '') + '</td>' +
+                '<td>' + escapeHtml(row.oprno || '') + '</td>' +
+                '<td title="' + escapeHtml(row.title || '') + '">' + escapeHtml(row.title || '') + '</td>' +
+                '<td>' + escapeHtml(row.result || row.content || '') + '</td>' +
+                '<td>' + escapeHtml(row.time || row.createDate || row.updateDate || '') + '</td>' +
                 '</tr>';
         }
         $('#ngTable').html(html);
     }
 
-    // 大屏核心格式化入口:
-    // 1. 从 screenData/hourlyData 取接口数据;
-    // 2. 对缺失字段做兜底和派生计算;
-    // 3. 写入顶部指标、表格和各个 ECharts 面板。
+    function startNgPageTimer() {
+        if (ngPageTimer) {
+            clearInterval(ngPageTimer);
+            ngPageTimer = null;
+        }
+        var pageCount = Math.ceil(ngRows.length / ngPageSize);
+        if (pageCount <= 1) return;
+        ngPageTimer = setInterval(function () {
+            ngPageIndex = (ngPageIndex + 1) % pageCount;
+            switchTablePage('#ngTable', renderNgTablePage);
+        }, 4200);
+    }
+
+    function switchTablePage(selector, renderer) {
+        var $body = $(selector);
+        $body.addClass('table-switching');
+        setTimeout(function () {
+            renderer();
+            $body.removeClass('table-switching');
+        }, 180);
+    }
+
     function formatData() {
-        // screenData.date/output/quality1/quality2 均来自 screenData4。
-        // date 是近七日横轴,output 是近七日产量,quality1/quality2 用于推算合格/不合格。
-        var dates = getArray(screenData.date, fallbackDates);
-        var output = getArray(screenData.output, [400, 354, 256, 164, 241, 342, 286]);
-        var quality1 = getArray(screenData.quality1, output.map(function (v) { return Math.round(v * .96); }));
-        var quality2 = getArray(screenData.quality2, output.map(function (v) { return Math.max(1, Math.round(v * .04)); }));
-
-        // 目前 screenData4 没有直接返回交付率/完成率曲线,因此先用产量数据派生展示。
-        // 后续若后台返回 deliveryRateList、finishRateList 等字段,可在这里直接替换 values。
-        var target = output.map(function () { return 1100; });
-        var actual = output.map(function (v) { return Math.max(980, v + 800); });
-        var plan = Number(screenData.plan) || 1200;
-        var today = Number(screenData.actual) || output[output.length - 1] || 0;
-        var month = sum(output) + today;
-        var finishRate = plan ? Math.min(100, Math.round(today * 100 / plan)) : 0;
-        var deliveryRate = Math.max(80, Math.min(99, finishRate || 98));
-        var iqcRate = Math.max(88, Math.min(99, 100 - Math.round(sum(quality2) * 100 / Math.max(1, sum(output)))));
-
-        // 写入大数字指标。today/month 等值来自接口或由近七日产量派生。
+        calculateTablePages();
+        var dates = getArray(screenData.date);
+        if (!dates.length) {
+            dates = recentDates(7);
+        }
+        var output = alignNumbers(screenData.output, dates.length);
+        var quality1 = alignNumbers(screenData.quality1, dates.length);
+        var quality2 = alignNumbers(screenData.quality2, dates.length);
+        var warehouseOut = alignNumbers(screenData.warehouseOut, dates.length);
+        var plan = numberValue(screenData.plan);
+        var today = numberValue(screenData.todayOutput || output[output.length - 1]);
+        var month = numberValue(screenData.monthOutput || sum(output));
+        var todayOut = numberValue(screenData.todayOut || warehouseOut[warehouseOut.length - 1]);
+        var monthOut = numberValue(screenData.monthOut || sum(warehouseOut));
+        var planTarget = dates.map(function () { return plan; });
+        var planRateValues = output.map(function (value) { return percent(value, plan); });
+        var finishRate = percent(today, plan);
+        var deliveryRate = percent(todayOut, today || plan);
+        var iqcRateValues = dates.map(function (_, i) { return percent(quality1[i], quality1[i] + quality2[i]); });
+        var iqcRate = iqcRateValues.length ? iqcRateValues[iqcRateValues.length - 1] : 0;
+
         $('#todayTotal').text(today);
         $('#monthTotal').text(month);
-        $('#todayOut').text(Math.max(0, today - 2));
-        $('#monthOut').text(Math.max(0, month - 8));
-        $('#deliveryRateText,#deliveryBadge').text(deliveryRate + '%');
-        $('#todayPlanRate').text(deliveryRate + '%');
-        $('#finishRate').text((finishRate || 98) + '%');
+        $('#todayOut').text(todayOut);
+        $('#monthOut').text(monthOut);
+        $('#deliveryRateText').text(deliveryRate + '%');
+        $('#todayPlanRate').text(finishRate + '%');
+        $('#finishRate').text(finishRate + '%');
         $('#iqcRate,#iqcRateText').text(iqcRate + '%');
 
         renderBars('workpieceChart', dates, output, '#83ff64', '#14a9ff');
-        renderLineArea('deliveryChart', dates, actual, target);
-        renderBars('warehouseChart', dates, output.map(function (v) { return Math.max(0, Math.round(v * .92)); }), '#86ff62', '#13a4ff');
-        renderLineArea('planRateChart', dates, actual, target);
-        renderLineArea('finishRateChart', dates, actual, target);
-        renderIqc(dates, quality2, dates.map(function (_, i) { return Math.max(88, Math.min(99, iqcRate - 3 + i)); }));
-
-        // 小时上下线接口返回多个产线分组,这里优先展示 HEVXT,没有则展示 B13XT。
-        // 如果要切换指定产线,调整这里的取值顺序即可。
+        renderLineArea('deliveryChart', dates, warehouseOut, output);
+        renderBars('warehouseChart', dates, warehouseOut, '#86ff62', '#13a4ff', {
+            top: 12,
+            left: 28,
+            right: 14,
+            bottom: 34,
+            containLabel: true
+        });
+        renderPercent('planRateChart', dates, planRateValues, 100);
+        renderLineArea('finishRateChart', dates, output, planTarget);
+        renderIqc(dates, quality2, iqcRateValues);
+
         var hLine = hourlyData.hevxt || hourlyData.b13xt || {};
-        renderDaily(getArray(hLine.hours, ['08:00','09:00','10:00','11:00','12:00','13:00','14:00']),
-            getArray(hLine.online, [70, 72, 68, 75, 78, 74, 80]),
-            getArray(hLine.offline, [63, 68, 70, 73, 71, 69, 72]));
+        var hours = getArray(hLine.hours);
+        renderDaily(hours, alignNumbers(hLine.online, hours.length), alignNumbers(hLine.offline, hours.length));
 
-        // 表格类数据:JPH 表格取工位数据,NG 表格临时复用 alarms。
         renderJphTable(screenData.oprnoData || screenData.workStationData);
-        renderNgTable(screenData.alarms);
+        renderNgTable(screenData.ngWarnings || screenData.alarms);
     }
 
-    // 前端数据获取入口。
-    // screenData4:综合数据接口,返回生产人数、设备数、计划/实际产量、近七日统计、质量、告警、工位数据等。
-    // screenDataHourly:小时维度上下线接口,返回每条产线的小时、上线数、下线数。
-    // 两个接口是异步返回的,所以每个接口成功后都调用 formatData(),谁先回来谁先渲染;
-    // 另一个接口回来后会再次刷新相关图表。
     function loadData() {
         $.post('/js/a/mes/mesProduct/screenData4', function (ret) {
             screenData = ret && ret.data ? ret.data : {};
             formatData();
         }).fail(function () {
-            // 接口失败时也渲染一次兜底数据,避免大屏白屏。
             screenData = {};
             formatData();
         });
@@ -914,6 +971,9 @@
         $.post('/js/a/mes/mesProduct/screenDataHourly', function (ret) {
             hourlyData = ret && ret.data ? ret.data : {};
             formatData();
+        }).fail(function () {
+            hourlyData = {};
+            formatData();
         });
     }
 
@@ -921,14 +981,13 @@
         fitScreen();
         updateClock();
         loadData();
-
-        // 时间只展示到分钟,20 秒刷新一次足够;业务数据每 2 分钟刷新一次。
         setInterval(updateClock, 1000 * 20);
         setInterval(loadData, 1000 * 60 * 2);
-
-        // 窗口尺寸变化时,重新缩放画布并通知 ECharts 重新计算尺寸。
         $(window).on('resize', function () {
             fitScreen();
+            calculateTablePages();
+            renderJphTablePage();
+            renderNgTablePage();
             Object.keys(chartMap).forEach(function (key) {
                 chartMap[key].resize();
             });