hfy 4 ماه پیش
والد
کامیت
05b08f273b

+ 74 - 22
src/main/java/com/jeesite/modules/mes/web/MesProductController.java

@@ -1,25 +1,16 @@
 package com.jeesite.modules.mes.web;
 
-import java.io.IOException;
-import java.text.DateFormat;
-import java.text.DecimalFormat;
-import java.text.SimpleDateFormat;
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
-import java.util.*;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
 import com.alibaba.fastjson.JSON;
 import com.jeesite.common.collect.ListUtils;
 import com.jeesite.common.collect.MapUtils;
+import com.jeesite.common.config.Global;
+import com.jeesite.common.entity.Page;
 import com.jeesite.common.lang.DateUtils;
 import com.jeesite.common.lang.ObjectUtils;
 import com.jeesite.common.lang.StringUtils;
 import com.jeesite.common.mybatis.mapper.query.QueryType;
-import com.jeesite.common.service.ServiceException;
 import com.jeesite.common.utils.excel.ExcelExport;
-import com.jeesite.modules.file.entity.FileUpload;
+import com.jeesite.common.web.BaseController;
 import com.jeesite.modules.mes.entity.*;
 import com.jeesite.modules.mes.resp.CommonResp;
 import com.jeesite.modules.mes.resp.MesShiftDataResp;
@@ -27,10 +18,6 @@ import com.jeesite.modules.mes.service.*;
 import com.jeesite.modules.mes.util.CommonUitl;
 import com.jeesite.modules.sys.entity.EmpUser;
 import com.jeesite.modules.sys.service.EmpUserService;
-import com.jeesite.modules.utils.NettyServerHandler;
-import com.jeesite.modules.utils.TesseractOcrConfiguration;
-import net.sourceforge.tess4j.TesseractException;
-import org.apache.poi.ss.formula.functions.T;
 import org.apache.shiro.authz.annotation.RequiresPermissions;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
@@ -41,11 +28,12 @@ import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
 
-import com.jeesite.common.config.Global;
-import com.jeesite.common.entity.Page;
-import com.jeesite.common.web.BaseController;
-import org.springframework.web.multipart.MultipartFile;
-import org.springframework.web.multipart.MultipartHttpServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
 
 /**
  * 产品Controller
@@ -317,6 +305,15 @@ public class MesProductController extends BaseController {
 		return "modules/mes/mesScreen4";
 	}
 
+	/**
+	 * 六安项目 MES 生产管理模块数据大屏入口。
+	 * 前端页面会继续调用 screenData4、screenDataHourly 等已有数据接口渲染图表。
+	 */
+	@RequestMapping(value = "screenLiuan")
+	public String screenLiuan(Model model) {
+		return "modules/mes/mesScreenLiuan";
+	}
+
 	@RequestMapping(value = "alarmData")
 	@ResponseBody
 	public CommonResp alarmData() {
@@ -746,6 +743,61 @@ public class MesProductController extends BaseController {
 		data.put("faultRates", faultRates);
 		data.put("faultDeviceCounts", faultDeviceCounts);
 
+		//根据产线编号查询工位号和产线名称
+		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");
+		List<MesLineProcess> processes = mesLineProcessService.findList(mesLineProcess);
+		processes.forEach(x->{
+			List<Object> row = new ArrayList<>();
+			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");
+			//获取每个工位当天实际产能
+			Calendar start = Calendar.getInstance();
+			start.set(Calendar.HOUR_OF_DAY, 0);
+			start.set(Calendar.MINUTE, 0);
+			start.set(Calendar.SECOND, 0);
+			start.set(Calendar.MILLISECOND, 0);
+
+			Calendar end = Calendar.getInstance();
+			end.set(Calendar.HOUR_OF_DAY, 23);
+			end.set(Calendar.MINUTE, 59);
+			end.set(Calendar.SECOND, 59);
+			end.set(Calendar.MILLISECOND, 999);
+
+			recorda.setUpdateDate_gte(start.getTime());
+			recorda.setUpdateDate_lte(end.getTime());
+			long count = mesProductRecordService.findCount(recorda);
+
+			MesProductRecord record12h=new MesProductRecord();
+			record12h.setOprno(x.getOprno());
+			record12h.setCraft("100000");
+			record12h.setContent("OK");
+			recorda.setLineSn("HEVXT");
+			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");
+			row.add(count);
+			row.add(count12h);
+
+			oprnoData.add(row);    // 把这一行数据加入List
+
+		});
+		data.put("oprnoData", oprnoData);
+
 		resp.setData(data);
 		resp.setResult(Global.TRUE);
 		return resp;
@@ -982,4 +1034,4 @@ public class MesProductController extends BaseController {
 		resp.setData(map);
 		return resp;
 	}
-}
+}

+ 5 - 1
src/main/resources/config/application.yml

@@ -622,7 +622,11 @@ shiro:
     ${adminPath}/mes/mesProductRecord/qmresult = anon
     ${adminPath}/mes/mesProductRecord/device = anon
     ${adminPath}/mes/mesProduct/screen = anon
+    ${adminPath}/mes/mesProduct/screenLiuan = anon
     ${adminPath}/mes/mesProduct/screenData = anon
+    ${adminPath}/mes/mesProduct/screenData4 = anon
+    ${adminPath}/mes/mesProduct/screenDataHourly = anon
+    ${adminPath}/mes/mesProduct/alarmData = anon
     ${adminPath}/mes/mesProductCcd/testDate = anon
     ${adminPath}/mes/mesProductCcd/add = anon
     ${adminPath}/mes/mesProductRecord/currentProduct = anon
@@ -1022,4 +1026,4 @@ msg:
 
 #======================================#
 #========== Project settings ==========#
-#======================================#
+#======================================#

BIN
src/main/resources/static/screen/imgs/liuan1.png


BIN
src/main/resources/static/screen/imgs/liuan10.png


BIN
src/main/resources/static/screen/imgs/liuan11.png


BIN
src/main/resources/static/screen/imgs/liuan12.png


BIN
src/main/resources/static/screen/imgs/liuan13.png


BIN
src/main/resources/static/screen/imgs/liuan14.png


BIN
src/main/resources/static/screen/imgs/liuan15.png


BIN
src/main/resources/static/screen/imgs/liuan2.png


BIN
src/main/resources/static/screen/imgs/liuan3.png


BIN
src/main/resources/static/screen/imgs/liuan4.png


BIN
src/main/resources/static/screen/imgs/liuan5.png


BIN
src/main/resources/static/screen/imgs/liuan6.png


BIN
src/main/resources/static/screen/imgs/liuan7.png


BIN
src/main/resources/static/screen/imgs/liuan8.png


BIN
src/main/resources/static/screen/imgs/liuan9.png


+ 939 - 0
src/main/resources/views/modules/mes/mesScreenLiuan.html

@@ -0,0 +1,939 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>MES生产管理模块数据大屏</title>
+    <style>
+        * {
+            box-sizing: border-box;
+        }
+        html,
+        body {
+            width: 100%;
+            height: 100%;
+            margin: 0;
+            overflow: hidden;
+            background: #020b20;
+            font-family: "Microsoft YaHei", Arial, sans-serif;
+        }
+        .liuan-screen {
+            position: relative;
+            width: 1920px;
+            height: 1080px;
+            overflow: hidden;
+            color: #fff;
+            transform-origin: left top;
+            background: #04122c url("${ctxStatic}/screen/imgs/liuan7.png") center / 100% 100% no-repeat;
+        }
+        .top-frame {
+            position: absolute;
+            left: 0;
+            top: 0;
+            width: 1920px;
+            height: 84px;
+            background: url("${ctxStatic}/screen/imgs/liuan6.png") center / 100% 100% no-repeat;
+            z-index: 3;
+        }
+        .brand {
+            position: absolute;
+            left: 70px;
+            top: 22px;
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            color: #fff;
+            font-size: 23px;
+            font-weight: 700;
+            letter-spacing: 1px;
+            z-index: 5;
+        }
+        .brand-mark {
+            width: 42px;
+            height: 24px;
+            border-radius: 50%;
+            border: 2px solid rgba(101, 222, 255, .8);
+            transform: rotate(-24deg);
+            position: relative;
+        }
+        .brand-mark::before,
+        .brand-mark::after {
+            content: "";
+            position: absolute;
+            border-radius: 50%;
+        }
+        .brand-mark::before {
+            width: 9px;
+            height: 9px;
+            left: 16px;
+            top: -4px;
+            background: #ffce35;
+            box-shadow: 13px 9px 0 #ff4058;
+        }
+        .brand-mark::after {
+            width: 32px;
+            height: 2px;
+            left: 5px;
+            top: 10px;
+            background: linear-gradient(90deg, transparent, #66ffe6, transparent);
+        }
+        .title {
+            position: absolute;
+            left: 50%;
+            top: 17px;
+            transform: translateX(-50%);
+            color: #f2fbff;
+            font-size: 29px;
+            line-height: 36px;
+            font-weight: 800;
+            letter-spacing: 2px;
+            text-shadow: 0 0 14px rgba(75, 198, 255, .9);
+            z-index: 5;
+        }
+        .clock {
+            position: absolute;
+            right: 72px;
+            top: 26px;
+            font-size: 18px;
+            color: rgba(255, 255, 255, .92);
+            letter-spacing: 1px;
+            z-index: 5;
+        }
+        .content {
+            position: absolute;
+            left: 48px;
+            right: 48px;
+            top: 98px;
+            bottom: 30px;
+        }
+        .panel {
+            position: absolute;
+            color: #e9fbff;
+        }
+        .panel-sm {
+            width: 390px;
+            height: 290px;
+            padding: 58px 25px 22px;
+        }
+        .panel-sm.bg-1 {
+            background: url("${ctxStatic}/screen/imgs/liuan1.png") top center / 390px 76px no-repeat,
+                        linear-gradient(180deg, rgba(11, 77, 143, .22), rgba(3, 28, 71, .62));
+            border: 1px solid rgba(0, 146, 255, .45);
+            box-shadow: inset 0 0 35px rgba(0, 135, 255, .17);
+        }
+        .panel-sm.bg-2 {
+            background: url("${ctxStatic}/screen/imgs/liuan2.png") top center / 390px 76px no-repeat,
+                        linear-gradient(180deg, rgba(11, 77, 143, .22), rgba(3, 28, 71, .62));
+            border: 1px solid rgba(0, 146, 255, .45);
+            box-shadow: inset 0 0 35px rgba(0, 135, 255, .17);
+        }
+        .panel-sm.bg-5 {
+            background: url("${ctxStatic}/screen/imgs/liuan5.png") top center / 390px 76px no-repeat,
+                        linear-gradient(180deg, rgba(11, 77, 143, .22), rgba(3, 28, 71, .62));
+            border: 1px solid rgba(0, 146, 255, .45);
+            box-shadow: inset 0 0 35px rgba(0, 135, 255, .17);
+        }
+        .panel-title {
+            position: absolute;
+            left: 0;
+            right: 0;
+            top: 11px;
+            text-align: center;
+            font-size: 18px;
+            font-weight: 800;
+            color: #ecfeff;
+            letter-spacing: 1px;
+            text-shadow: 0 0 10px rgba(0, 222, 255, .8);
+        }
+        .panel-title small {
+            display: block;
+            margin-top: 3px;
+            color: rgba(255, 255, 255, .75);
+            font-size: 12px;
+            font-weight: 600;
+        }
+        .left-top { left: 0; top: 0; }
+        .left-mid { left: 0; top: 315px; height: 285px; }
+        .left-bottom { left: 0; bottom: 0; height: 290px; }
+        .right-top { right: 0; top: 0; width: 454px; height: 310px; padding: 54px 24px 20px; background: url("${ctxStatic}/screen/imgs/liuan14.png") center / 100% 100% no-repeat; }
+        .right-mid { right: 0; top: 335px; height: 285px; }
+        .right-bottom { right: 0; bottom: 0; height: 290px; }
+        .center-main {
+            left: 472px;
+            top: 18px;
+            width: 905px;
+            height: 630px;
+            padding: 76px 38px 28px;
+            background: url("${ctxStatic}/screen/imgs/liuan15.png") center / 100% 100% no-repeat;
+        }
+        .bottom-card {
+            width: 438px;
+            height: 258px;
+            bottom: 0;
+            padding: 54px 22px 18px;
+            background: linear-gradient(180deg, rgba(11, 77, 143, .22), rgba(3, 28, 71, .62));
+            border: 1px solid rgba(0, 146, 255, .45);
+            box-shadow: inset 0 0 35px rgba(0, 135, 255, .17);
+        }
+        .bottom-1 { left: 430px; }
+        .bottom-2 { left: 902px; }
+        .metric-row {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            gap: 16px;
+            height: 72px;
+            margin-bottom: 10px;
+        }
+        .metric {
+            display: grid;
+            grid-template-columns: 56px 1fr;
+            align-items: center;
+            min-width: 0;
+        }
+        .metric img {
+            width: 52px;
+            height: 52px;
+        }
+        .metric-label {
+            color: #d9faff;
+            font-size: 14px;
+            line-height: 20px;
+        }
+        .metric-value {
+            color: #20f4ff;
+            font-size: 20px;
+            line-height: 26px;
+            font-weight: 800;
+            text-shadow: 0 0 10px rgba(37, 235, 255, .85);
+        }
+        .chart {
+            width: 100%;
+            height: 100%;
+        }
+        .left-top .chart {
+            height: 178px;
+        }
+        .chart-with-metrics {
+            position: absolute;
+            left: 20px;
+            right: 20px;
+            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;
+            right: 0;
+            top: 18px;
+            text-align: center;
+            color: #fff;
+            font-size: 18px;
+            font-weight: 900;
+            letter-spacing: 1px;
+        }
+        .jph-table {
+            width: 100%;
+            border-collapse: collapse;
+            color: #f3fcff;
+            table-layout: fixed;
+        }
+        .jph-table th {
+            height: 47px;
+            color: #ffffff;
+            font-size: 15px;
+            font-weight: 800;
+            text-align: center;
+            background: rgba(8, 102, 173, .35);
+        }
+        .jph-table td {
+            height: 52px;
+            text-align: center;
+            font-size: 20px;
+            font-weight: 800;
+            border-bottom: 1px solid rgba(73, 193, 255, .12);
+        }
+        .jph-table tbody tr:nth-child(odd) td {
+            background: rgba(3, 75, 139, .42);
+        }
+        .jph-table tbody tr:nth-child(even) td {
+            background: rgba(2, 44, 102, .45);
+        }
+        .jph-table td.name {
+            text-align: left;
+            padding-left: 14px;
+            font-size: 18px;
+        }
+        .ng-table {
+            width: 100%;
+            table-layout: fixed;
+            border-collapse: collapse;
+        }
+        .ng-table th {
+            color: #c8efff;
+            font-size: 12px;
+            height: 28px;
+            text-align: center;
+        }
+        .ng-table td {
+            height: 30px;
+            color: #fff;
+            font-size: 12px;
+            text-align: center;
+            border-bottom: 1px solid rgba(61, 177, 255, .12);
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+        .ng-table tbody tr:nth-child(odd) td {
+            background: rgba(10, 88, 153, .36);
+        }
+        .mini-stat {
+            position: absolute;
+            left: 24px;
+            top: 74px;
+            display: grid;
+            grid-template-columns: 54px 1fr;
+            gap: 10px;
+            align-items: center;
+        }
+        .mini-stat img {
+            width: 48px;
+            height: 48px;
+        }
+        .mini-stat span {
+            display: block;
+            color: #dffcff;
+            font-size: 15px;
+            line-height: 20px;
+        }
+        .mini-stat strong {
+            color: #20f4ff;
+            font-size: 22px;
+        }
+        .legend {
+            position: absolute;
+            right: 32px;
+            top: 96px;
+            display: flex;
+            gap: 14px;
+            color: #dcefff;
+            font-size: 11px;
+        }
+        .legend i {
+            display: inline-block;
+            width: 12px;
+            height: 6px;
+            margin-right: 4px;
+            border-radius: 4px;
+            vertical-align: middle;
+        }
+        .line-green { background: #10f2b4; }
+        .line-yellow { background: #ffc82e; }
+        .line-blue { background: #18b8ff; }
+        .rate-number {
+            position: absolute;
+            right: 42px;
+            top: 85px;
+            color: #23f8ff;
+            font-size: 24px;
+            font-weight: 900;
+        }
+        .daily-legend {
+            position: absolute;
+            right: 34px;
+            top: 72px;
+            color: #dcefff;
+            font-size: 12px;
+            display: flex;
+            gap: 16px;
+        }
+        .daily-legend span::before {
+            content: "";
+            display: inline-block;
+            width: 10px;
+            height: 10px;
+            margin-right: 5px;
+            border-radius: 50%;
+            vertical-align: -1px;
+        }
+        .daily-legend .online::before { background: #10f2b4; }
+        .daily-legend .offline::before { background: #18b8ff; }
+    </style>
+</head>
+<body>
+<div class="liuan-screen" id="screen">
+    <div class="top-frame"></div>
+    <div class="brand"><span class="brand-mark"></span><span>大洋MES</span></div>
+    <div class="title">MES生产管理模块数据大屏</div>
+    <div class="clock" id="clock"></div>
+
+    <div class="content">
+        <section class="panel panel-sm bg-1 left-top">
+            <div class="panel-title">工件数据统计</div>
+            <div class="metric-row">
+                <div class="metric">
+                    <img src="${ctxStatic}/screen/imgs/liuan8.png" alt="">
+                    <div><div class="metric-label">今日总产量</div><div class="metric-value" id="todayTotal">0</div></div>
+                </div>
+                <div class="metric">
+                    <img src="${ctxStatic}/screen/imgs/liuan9.png" alt="">
+                    <div><div class="metric-label">本月总产量</div><div class="metric-value" id="monthTotal">0</div></div>
+                </div>
+            </div>
+            <div class="chart" id="workpieceChart"></div>
+        </section>
+
+        <section class="panel panel-sm bg-2 left-mid">
+            <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>
+            <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>
+
+        <section class="panel panel-sm bg-5 left-bottom">
+            <div class="panel-title">出库数据统计</div>
+            <div class="metric-row">
+                <div class="metric">
+                    <img src="${ctxStatic}/screen/imgs/liuan11.png" alt="">
+                    <div><div class="metric-label">今日出库</div><div class="metric-value" id="todayOut">0</div></div>
+                </div>
+                <div class="metric">
+                    <img src="${ctxStatic}/screen/imgs/liuan12.png" alt="">
+                    <div><div class="metric-label">本月出库</div><div class="metric-value" id="monthOut">0</div></div>
+                </div>
+            </div>
+            <div class="chart" id="warehouseChart"></div>
+        </section>
+
+        <section class="panel center-main">
+            <div class="table-title">HEV各工位实际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>
+                </tr>
+                </thead>
+                <tbody id="jphTable"></tbody>
+            </table>
+        </section>
+
+        <section class="panel right-top">
+            <div class="panel-title">NG预警记录<small>(超48小时未处理记录)</small></div>
+            <table class="ng-table">
+                <thead>
+                <tr>
+                    <th>工件号</th>
+                    <th>工位号</th>
+                    <th>提交名称</th>
+                    <th>结果</th>
+                    <th>预警时间</th>
+                </tr>
+                </thead>
+                <tbody id="ngTable"></tbody>
+            </table>
+        </section>
+
+        <section class="panel panel-sm bg-2 right-mid">
+            <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>
+            <div class="rate-number" id="iqcRate">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="iqcChart"></div>
+        </section>
+
+        <section class="panel panel-sm bg-5 right-bottom">
+            <div class="panel-title">每日上下线统计</div>
+            <div class="daily-legend"><span class="online">上线</span><span class="offline">下线</span></div>
+            <div class="chart" id="dailyChart"></div>
+        </section>
+
+        <section class="panel bottom-card bottom-1">
+            <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>
+            <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>
+        </section>
+
+        <section class="panel bottom-card bottom-2">
+            <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>
+            <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>
+        </section>
+    </div>
+</div>
+
+<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 jphPageIndex = 0;
+    var jphRows = [];
+    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']
+    ];
+
+    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 + ')'
+        });
+    }
+
+    function pad(n) {
+        return n < 10 ? '0' + n : '' + n;
+    }
+
+    function updateClock() {
+        var now = new Date();
+        var weeks = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
+        $('#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 sum(list) {
+        list = list || [];
+        var total = 0;
+        for (var i = 0; i < list.length; i++) {
+            total += Number(list[i]) || 0;
+        }
+        return total;
+    }
+
+    function chart(id, option) {
+        var el = document.getElementById(id);
+        if (!el) return;
+        if (!chartMap[id]) {
+            chartMap[id] = echarts.init(el);
+        }
+        chartMap[id].setOption(option, true);
+    }
+
+    // 图表通用网格配置,保证不同图表的坐标轴留白一致。
+    function baseGrid(bottom) {
+        return {
+            top: 18,
+            left: 35,
+            right: 16,
+            bottom: bottom || 28,
+            containLabel: true
+        };
+    }
+
+    // 图表通用坐标轴样式,匹配蓝湖稿的蓝色科技风。
+    function axisStyle() {
+        return {
+            axisLine: { lineStyle: { color: 'rgba(97, 199, 255, .35)' } },
+            axisTick: { show: false },
+            axisLabel: { color: '#bdeeff', fontSize: 10 },
+            splitLine: { lineStyle: { color: 'rgba(75, 164, 255, .12)' } }
+        };
+    }
+
+    // 渲染柱状图:用于工件数据统计、出库数据统计等只需要一组数值的面板。
+    function renderBars(id, dates, values, color1, color2) {
+        chart(id, {
+            grid: baseGrid(22),
+            tooltip: { trigger: 'axis' },
+            xAxis: Object.assign({ type: 'category', data: dates }, axisStyle()),
+            yAxis: Object.assign({ type: 'value' }, axisStyle()),
+            series: [{
+                type: 'bar',
+                data: values,
+                barWidth: 14,
+                itemStyle: {
+                    barBorderRadius: [8, 8, 0, 0],
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                        { offset: 0, color: color1 },
+                        { offset: 1, color: color2 }
+                    ])
+                }
+            }]
+        });
+    }
+
+    // 渲染“实际/目标”双线面积图:用于计划交付率、计划达成率、计划完成率。
+    function renderLineArea(id, dates, values, target) {
+        chart(id, {
+            grid: baseGrid(22),
+            tooltip: { trigger: 'axis' },
+            xAxis: Object.assign({ type: 'category', boundaryGap: false, data: dates }, axisStyle()),
+            yAxis: Object.assign({ type: 'value' }, axisStyle()),
+            series: [
+                {
+                    name: '实际',
+                    type: 'line',
+                    smooth: true,
+                    data: values,
+                    symbol: 'circle',
+                    symbolSize: 7,
+                    lineStyle: { color: '#15f2b8', width: 3 },
+                    itemStyle: { color: '#15f2b8' },
+                    areaStyle: {
+                        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                            { offset: 0, color: 'rgba(21,242,184,.42)' },
+                            { offset: 1, color: 'rgba(21,242,184,.02)' }
+                        ])
+                    }
+                },
+                {
+                    name: '目标',
+                    type: 'line',
+                    smooth: true,
+                    data: target || values.map(function () { return 1100; }),
+                    symbol: 'none',
+                    lineStyle: { color: '#ffcf30', width: 2 }
+                }
+            ]
+        });
+    }
+
+    // 渲染百分比折线图。当前页面未直接调用,保留给后续接入纯百分比指标时复用。
+    function renderPercent(id, dates, values) {
+        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)' }
+                    ])
+                }
+            }]
+        });
+    }
+
+    // 渲染 IQC 合格率:柱子表示不合格数量,折线表示合格率。
+    function renderIqc(dates, bad, rates) {
+        chart('iqcChart', {
+            grid: baseGrid(24),
+            tooltip: { trigger: 'axis' },
+            xAxis: Object.assign({ type: 'category', data: dates }, axisStyle()),
+            yAxis: [
+                Object.assign({ type: 'value' }, axisStyle()),
+                Object.assign({ type: 'value', min: 0, max: 100, axisLabel: { color: '#bdeeff', formatter: '{value}%' } }, axisStyle())
+            ],
+            series: [
+                {
+                    type: 'bar',
+                    name: '不合格数量',
+                    data: bad,
+                    barWidth: 12,
+                    itemStyle: {
+                        barBorderRadius: [8, 8, 0, 0],
+                        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                            { offset: 0, color: '#7cff55' },
+                            { offset: 1, color: '#0ea4ff' }
+                        ])
+                    }
+                },
+                {
+                    type: 'line',
+                    name: '合格率',
+                    yAxisIndex: 1,
+                    smooth: true,
+                    data: rates,
+                    lineStyle: { color: '#ffc72e', width: 3 },
+                    itemStyle: { color: '#ffc72e' }
+                }
+            ]
+        });
+    }
+
+    // 渲染每日上下线统计。数据来自 screenDataHourly:
+    // hLine.hours 为横轴小时,hLine.online 为上线数,hLine.offline 为下线数。
+    function renderDaily(dates, online, offline) {
+        chart('dailyChart', {
+            grid: baseGrid(20),
+            tooltip: { trigger: 'axis' },
+            xAxis: Object.assign({ type: 'category', boundaryGap: false, data: dates }, axisStyle()),
+            yAxis: Object.assign({ type: 'value' }, axisStyle()),
+            series: [
+                {
+                    name: '上线',
+                    type: 'line',
+                    smooth: true,
+                    data: online,
+                    symbol: 'none',
+                    lineStyle: { color: '#13f0b2', width: 3 },
+                    areaStyle: { color: 'rgba(19, 240, 178, .22)' }
+                },
+                {
+                    name: '下线',
+                    type: 'line',
+                    smooth: true,
+                    data: offline,
+                    symbol: 'none',
+                    lineStyle: { color: '#1aa9ff', width: 3 },
+                    areaStyle: { color: 'rgba(26, 169, 255, .20)' }
+                }
+            ]
+        });
+    }
+
+    // 渲染中间 JPH 表格。rows 优先取 screenData.oprnoData,
+    // 若旧接口没有该字段,则兼容使用 screenData.workStationData。
+    function renderJphTable(rows) {
+        jphRows = [];
+        var sourceRows = rows && rows.length ? rows : [];
+        var maxLength =  sourceRows.length;
+
+        for (var i = 0; i < maxLength; i++) {
+            var item = sourceRows[i] || {};
+            jphRows.push([
+                item[0],
+                item[1],
+                item[2],
+                item[3],
+                item[4],
+                item[5]
+            ]);
+        }
+
+        jphPageIndex = 0;
+        renderJphTablePage();
+        startJphPageTimer();
+    }
+
+    function renderJphTablePage() {
+        var html = '';
+        var start = jphPageIndex * jphPageSize;
+        var pageRows = jphRows.slice(start, start + jphPageSize);
+
+        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>' +
+                '</tr>';
+        }
+        $('#jphTable').html(html);
+    }
+
+    function startJphPageTimer() {
+        if (jphPageTimer) {
+            clearInterval(jphPageTimer);
+            jphPageTimer = null;
+        }
+
+        var pageCount = Math.ceil(jphRows.length / jphPageSize);
+        if (pageCount <= 1) {
+            return;
+        }
+
+        jphPageTimer = setInterval(function () {
+            jphPageIndex = (jphPageIndex + 1) % pageCount;
+            renderJphTablePage();
+        }, 5000);
+    }
+
+    // 渲染右上 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' }
+        ]);
+        var html = '';
+        for (var i = 0; i < Math.min(source.length, 5); i++) {
+            var row = source[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>' +
+                '</tr>';
+        }
+        $('#ngTable').html(html);
+    }
+
+    // 大屏核心格式化入口:
+    // 1. 从 screenData/hourlyData 取接口数据;
+    // 2. 对缺失字段做兜底和派生计算;
+    // 3. 写入顶部指标、表格和各个 ECharts 面板。
+    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 等值来自接口或由近七日产量派生。
+        $('#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) + '%');
+        $('#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。
+        // 如果要切换指定产线,调整这里的取值顺序即可。
+        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]));
+
+        // 表格类数据:JPH 表格取工位数据,NG 表格临时复用 alarms。
+        renderJphTable(screenData.oprnoData || screenData.workStationData);
+        renderNgTable(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();
+        });
+
+        $.post('/js/a/mes/mesProduct/screenDataHourly', function (ret) {
+            hourlyData = ret && ret.data ? ret.data : {};
+            formatData();
+        });
+    }
+
+    $(function () {
+        fitScreen();
+        updateClock();
+        loadData();
+
+        // 时间只展示到分钟,20 秒刷新一次足够;业务数据每 2 分钟刷新一次。
+        setInterval(updateClock, 1000 * 20);
+        setInterval(loadData, 1000 * 60 * 2);
+
+        // 窗口尺寸变化时,重新缩放画布并通知 ECharts 重新计算尺寸。
+        $(window).on('resize', function () {
+            fitScreen();
+            Object.keys(chartMap).forEach(function (key) {
+                chartMap[key].resize();
+            });
+        });
+    });
+</script>
+</body>
+</html>