Browse Source

综合数据大屏

hfy 4 months ago
parent
commit
f98d979743
29 changed files with 996 additions and 2 deletions
  1. 133 2
      src/main/java/com/jeesite/modules/mes/web/MesProductController.java
  2. BIN
      src/main/resources/static/screen/imgs/screenzong1.png
  3. BIN
      src/main/resources/static/screen/imgs/screenzong10.png
  4. BIN
      src/main/resources/static/screen/imgs/screenzong11.png
  5. BIN
      src/main/resources/static/screen/imgs/screenzong12.png
  6. BIN
      src/main/resources/static/screen/imgs/screenzong13.png
  7. BIN
      src/main/resources/static/screen/imgs/screenzong14.png
  8. BIN
      src/main/resources/static/screen/imgs/screenzong15.png
  9. BIN
      src/main/resources/static/screen/imgs/screenzong16.png
  10. BIN
      src/main/resources/static/screen/imgs/screenzong17.png
  11. BIN
      src/main/resources/static/screen/imgs/screenzong18.png
  12. BIN
      src/main/resources/static/screen/imgs/screenzong19.png
  13. BIN
      src/main/resources/static/screen/imgs/screenzong2.png
  14. BIN
      src/main/resources/static/screen/imgs/screenzong20.png
  15. BIN
      src/main/resources/static/screen/imgs/screenzong21.png
  16. BIN
      src/main/resources/static/screen/imgs/screenzong22.png
  17. BIN
      src/main/resources/static/screen/imgs/screenzong23.png
  18. BIN
      src/main/resources/static/screen/imgs/screenzong24.png
  19. BIN
      src/main/resources/static/screen/imgs/screenzong25.png
  20. BIN
      src/main/resources/static/screen/imgs/screenzong26.png
  21. BIN
      src/main/resources/static/screen/imgs/screenzong27.png
  22. BIN
      src/main/resources/static/screen/imgs/screenzong3.png
  23. BIN
      src/main/resources/static/screen/imgs/screenzong4.png
  24. BIN
      src/main/resources/static/screen/imgs/screenzong5.png
  25. BIN
      src/main/resources/static/screen/imgs/screenzong6.png
  26. BIN
      src/main/resources/static/screen/imgs/screenzong7.png
  27. BIN
      src/main/resources/static/screen/imgs/screenzong8.png
  28. BIN
      src/main/resources/static/screen/imgs/screenzong9.png
  29. 863 0
      src/main/resources/views/modules/mes/mesIntegratedScreen.html

+ 133 - 2
src/main/java/com/jeesite/modules/mes/web/MesProductController.java

@@ -349,6 +349,73 @@ public class MesProductController extends BaseController {
 		return "modules/mes/mesScreenLiuan";
 	}
 
+	@RequestMapping(value = "integratedScreen")
+	public String integratedScreen(Model model) {
+		return "modules/mes/mesIntegratedScreen";
+	}
+
+	@RequestMapping(value = "integratedScreenData")
+	@ResponseBody
+	public CommonResp<Map<String, Object>> integratedScreenData() {
+		CommonResp<Map<String, Object>> resp = new CommonResp<>();
+		CommonResp baseResp = screenData4();
+		Map<String, Object> data = baseResp.getData() instanceof Map
+				? new LinkedHashMap<>((Map<String, Object>) baseResp.getData())
+				: new LinkedHashMap<>();
+
+		Map<String, Map<String, Object>> orderDetailMap = new LinkedHashMap<>();
+		List<MesProduct> productList = mesProductService.findList(new MesProduct());
+		if (productList != null) {
+			for (MesProduct product : productList) {
+				String name = safeText(product.getTitle(), safeText(product.getCate(), "-"));
+				String orderNo = safeText(product.getPsn(), safeText(product.getBatchSn(), safeText(product.getSn(), "-")));
+				String key = orderNo + "|" + name;
+				Map<String, Object> row = orderDetailMap.get(key);
+				if (row == null) {
+					if (orderDetailMap.size() >= 6) {
+						continue;
+					}
+					row = MapUtils.newHashMap();
+					row.put("name", name);
+					row.put("orderNo", orderNo);
+					row.put("done", 0);
+					row.put("state", productStateLabel(product.getState()));
+					orderDetailMap.put(key, row);
+				}
+				row.put("done", parseInt(String.valueOf(row.get("done"))) + 1);
+				if ("0".equals(product.getState())) {
+					row.put("state", productStateLabel(product.getState()));
+				}
+				if (orderDetailMap.size() >= 6 && orderDetailMap.containsKey(key)) {
+					break;
+				}
+			}
+		}
+		data.put("orderDetails", new ArrayList<>(orderDetailMap.values()));
+
+		List<Map<String, Object>> warehouseRows = new ArrayList<>();
+		Map<String, Object> todayOut = MapUtils.newHashMap();
+		todayOut.put("name", "今日出库");
+		todayOut.put("value", data.get("todayOut"));
+		todayOut.put("unit", "件");
+		warehouseRows.add(todayOut);
+		Map<String, Object> monthOut = MapUtils.newHashMap();
+		monthOut.put("name", "本月出库");
+		monthOut.put("value", data.get("monthOut"));
+		monthOut.put("unit", "件");
+		warehouseRows.add(monthOut);
+		data.put("warehouseRows", warehouseRows);
+
+		Map<String, Object> hourlyOkData = buildStartEndOprnoOkHourlyData();
+		data.put("hourTimes", hourlyOkData.get("hours"));
+		data.put("onlineHourlyData", hourlyOkData.get("online"));
+		data.put("offlineHourlyData", hourlyOkData.get("offline"));
+
+		resp.setData(data);
+		resp.setResult(Global.TRUE);
+		return resp;
+	}
+
 	@RequestMapping(value = "qualityScreen")
 	public String qualityScreen(Model model) {
 		return "modules/mes/mesQualityScreen";
@@ -1445,6 +1512,66 @@ public class MesProductController extends BaseController {
 		return parseInt(digits);
 	}
 
+	private String productStateLabel(String state) {
+		if ("1".equals(state)) {
+			return "已完成";
+		}
+		if ("0".equals(state)) {
+			return "生产中";
+		}
+		if ("2".equals(state)) {
+			return "已入库";
+		}
+		return StringUtils.isEmpty(state) ? "-" : state;
+	}
+
+	private Map<String, Object> buildStartEndOprnoOkHourlyData() {
+		String[] lineNames = {"HEVXT", "115XT"};
+		String[] startOprnos = {"OP010", "OP010"};
+		String[] endOprnos = {"OP220", "OP280"};
+		List<String> hourLabels = new ArrayList<>();
+		List<Integer> onlineCounts = new ArrayList<>();
+		List<Integer> offlineCounts = new ArrayList<>();
+
+		for (int h = 0; h < 24; h++) {
+			Calendar hourStart = Calendar.getInstance();
+			hourStart.set(Calendar.HOUR_OF_DAY, h);
+			hourStart.set(Calendar.MINUTE, 0);
+			hourStart.set(Calendar.SECOND, 0);
+			hourStart.set(Calendar.MILLISECOND, 0);
+
+			Calendar hourEnd = (Calendar) hourStart.clone();
+			hourEnd.add(Calendar.HOUR_OF_DAY, 1);
+
+			hourLabels.add(String.format("%02d:00", h));
+			int onlineCount = 0;
+			int offlineCount = 0;
+			for (int i = 0; i < lineNames.length; i++) {
+				onlineCount += countOprnoOk(lineNames[i], startOprnos[i], hourStart.getTime(), hourEnd.getTime());
+				offlineCount += countOprnoOk(lineNames[i], endOprnos[i], hourStart.getTime(), hourEnd.getTime());
+			}
+			onlineCounts.add(onlineCount);
+			offlineCounts.add(offlineCount);
+		}
+
+		Map<String, Object> data = MapUtils.newHashMap();
+		data.put("hours", hourLabels);
+		data.put("online", onlineCounts);
+		data.put("offline", offlineCounts);
+		return data;
+	}
+
+	private int countOprnoOk(String lineSn, String oprno, Date start, Date end) {
+		MesProductRecord query = new MesProductRecord();
+		query.setLineSn(lineSn);
+		query.setOprno(oprno);
+		query.setCraft("100000");
+		query.setContent("OK");
+		query.setCreateDate_gte(start);
+		query.setCreateDate_lte(end);
+		return (int) mesProductRecordService.findCount(query);
+	}
+
 	@RequestMapping(value = "screenDataHourly")
 	@ResponseBody
 	public CommonResp screenDataHourly() {
@@ -1460,7 +1587,7 @@ public class MesProductController extends BaseController {
 		cal.add(Calendar.DAY_OF_MONTH, 1);
 		Date todayEnd = cal.getTime();
 		
-		String[] lineNames = {"HEVXT", "B13XT"};
+		String[] lineNames = {"HEVXT", "115XT"};
 		String[] offlineOprnos = {"OP220", "OP280"};
 		
 		for (int lineIdx = 0; lineIdx < lineNames.length; lineIdx++) {
@@ -1487,7 +1614,9 @@ public class MesProductController extends BaseController {
 				// 查询上线工位(OP010)记录数
 				MesProductRecord onlineQuery = new MesProductRecord();
 				onlineQuery.setLineSn(lineSn);
-				onlineQuery.setOprno("OP010A");
+				onlineQuery.setOprno("OP010");
+				onlineQuery.setCraft("100000");
+				onlineQuery.setContent("OK");
 				onlineQuery.setCreateDate_gte(hourStart.getTime());
 				onlineQuery.setCreateDate_lte(hourEnd.getTime());
 				long onlineCount = mesProductRecordService.findCount(onlineQuery);
@@ -1497,6 +1626,8 @@ public class MesProductController extends BaseController {
 				MesProductRecord offlineQuery = new MesProductRecord();
 				offlineQuery.setLineSn(lineSn);
 				offlineQuery.setOprno(offlineOprno);
+				offlineQuery.setCraft("100000");
+				offlineQuery.setContent("OK");
 				offlineQuery.setCreateDate_gte(hourStart.getTime());
 				offlineQuery.setCreateDate_lte(hourEnd.getTime());
 				long offlineCount = mesProductRecordService.findCount(offlineQuery);

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


+ 863 - 0
src/main/resources/views/modules/mes/mesIntegratedScreen.html

@@ -0,0 +1,863 @@
+<!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: #03133a;
+			color: #f2fbff;
+			font-family: "Microsoft YaHei", Arial, sans-serif;
+		}
+		body {
+			width: 1920px;
+			height: 1080px;
+			transform-origin: 0 0;
+		}
+		.screen {
+			position: relative;
+			width: 1920px;
+			height: 1080px;
+			overflow: hidden;
+			background: #03133a url("${ctxStatic}/screen/imgs/screenzong27.png") center / 100% 100% no-repeat;
+		}
+		.header {
+			position: absolute;
+			left: 0;
+			top: 0;
+			width: 1920px;
+			height: 100px;
+			background: url("${ctxStatic}/screen/imgs/screenzong5.png") center / 100% 100% no-repeat;
+			z-index: 5;
+		}
+		.logo {
+			position: absolute;
+			left: 28px;
+			top: 26px;
+			width: 176px;
+			height: 42px;
+			background: url("${ctxStatic}/screen/imgs/dyLogo.png") center / contain no-repeat;
+		}
+		.title {
+			position: absolute;
+			left: 50%;
+			top: 27px;
+			transform: translateX(-50%);
+			font-size: 35px;
+			line-height: 45px;
+			font-weight: 900;
+			letter-spacing: 7px;
+			color: #fff;
+			text-shadow: 0 0 16px rgba(86, 205, 255, .9);
+		}
+		.clock {
+			position: absolute;
+			right: 32px;
+			top: 37px;
+			font-size: 23px;
+			line-height: 28px;
+			color: rgba(255, 255, 255, .95);
+		}
+		.panel {
+			position: absolute;
+			z-index: 3;
+			overflow: hidden;
+			background: linear-gradient(180deg, rgba(17, 85, 180, .76), rgba(9, 61, 150, .72));
+			border: 1px solid rgba(46, 156, 255, .34);
+			box-shadow: inset 0 0 34px rgba(31, 163, 255, .2);
+		}
+		.panel:after {
+			content: "";
+			position: absolute;
+			right: 10px;
+			top: 10px;
+			width: 13px;
+			height: 13px;
+			background: linear-gradient(#1787ff 0 2px, transparent 2px 5px, #1787ff 5px 7px, transparent 7px 10px, #1787ff 10px 12px);
+			opacity: .8;
+		}
+		.panel-title {
+			position: relative;
+			height: 40px;
+			line-height: 40px;
+			padding-left: 36px;
+			font-size: 22px;
+			font-weight: 900;
+			color: #fff;
+			background: linear-gradient(110deg, rgba(25, 143, 255, .9), rgba(23, 97, 214, .66) 48%, rgba(19, 88, 197, .1) 49%, transparent 100%);
+			text-shadow: 0 0 10px rgba(91, 223, 255, .95);
+		}
+		.panel-title:before {
+			content: "";
+			position: absolute;
+			left: 13px;
+			top: 15px;
+			width: 10px;
+			height: 10px;
+			background: #b4f9ff;
+			transform: rotate(45deg);
+			box-shadow: 0 0 10px #54ecff;
+		}
+		.panel-body {
+			position: relative;
+			height: calc(100% - 40px);
+			padding: 14px 24px 18px;
+		}
+		.left-top { left: 31px; top: 110px; width: 440px; height: 377px; }
+		.left-middle { left: 31px; top: 506px; width: 440px; height: 226px; }
+		.left-bottom { left: 31px; top: 753px; width: 440px; height: 298px; }
+		.right-top { right: 30px; top: 110px; width: 440px; height: 377px; }
+		.right-middle { right: 30px; top: 506px; width: 440px; height: 297px; }
+		.right-bottom { right: 30px; top: 823px; width: 440px; height: 227px; }
+		.bottom-left { left: 489px; top: 753px; width: 447px; height: 298px; }
+		.bottom-right { left: 956px; top: 753px; width: 475px; height: 298px; }
+		.left-top { background-image: url("${ctxStatic}/screen/imgs/screenzong19.png"); background-size: 100% 100%; }
+		.left-middle { background-image: url("${ctxStatic}/screen/imgs/screenzong7.png"); background-size: 100% 100%; }
+		.left-bottom, .right-middle, .bottom-left { background-image: url("${ctxStatic}/screen/imgs/screenzong20.png"); background-size: 100% 100%; }
+		.right-top { background-image: url("${ctxStatic}/screen/imgs/screenzong19.png"); background-size: 100% 100%; }
+		.right-bottom { background-image: url("${ctxStatic}/screen/imgs/screenzong7.png"); background-size: 100% 100%; }
+		.bottom-right { background-image: url("${ctxStatic}/screen/imgs/screenzong24.png"); background-size: 100% 100%; }
+		.kpi-row {
+			display: grid;
+			grid-template-columns: 1fr 1fr;
+			gap: 26px;
+			margin: 16px 10px 12px;
+		}
+		.kpi {
+			display: grid;
+			grid-template-columns: 64px 1fr;
+			align-items: center;
+			min-width: 0;
+		}
+		.kpi img {
+			width: 58px;
+			height: 58px;
+			object-fit: contain;
+		}
+		.kpi-label {
+			font-size: 14px;
+			line-height: 22px;
+			color: #d7f7ff;
+			font-weight: 700;
+		}
+		.kpi-value {
+			margin-top: 3px;
+			font-size: 24px;
+			line-height: 30px;
+			font-weight: 900;
+			color: #fff;
+			text-shadow: 0 0 10px rgba(80, 218, 255, .85);
+		}
+		.kpi-unit {
+			margin-left: 5px;
+			font-size: 12px;
+			color: #c5edff;
+		}
+		.progress-wrap { margin: 6px 10px 0; }
+		.progress-line {
+			height: 12px;
+			background: rgba(5, 51, 118, .86);
+			overflow: hidden;
+		}
+		.progress-line span {
+			display: block;
+			width: 0;
+			height: 100%;
+			background: linear-gradient(90deg, #15a8ff, #e9efff);
+		}
+		.progress-meta {
+			display: grid;
+			grid-template-columns: 1fr 1fr;
+			margin-top: 5px;
+			align-items: center;
+			font-size: 15px;
+			color: #fff;
+			font-weight: 800;
+		}
+		.progress-meta strong {
+			justify-self: end;
+			font-size: 26px;
+		}
+		.chart { width: 100%; height: 100%; }
+		#capacityChart { height: 146px; margin-top: 23px; }
+		#hourChart { height: 226px; }
+		#repairTopChart { position: absolute; left: 20px; top: 58px; width: 210px; height: 210px; }
+		.tabs {
+			position: absolute;
+			right: 24px;
+			top: 156px;
+			display: flex;
+			gap: 12px;
+			font-size: 15px;
+			font-weight: 800;
+		}
+		.tabs span {
+			padding: 2px 8px;
+			color: #fff;
+			border-bottom: 2px solid rgba(121, 226, 255, .65);
+		}
+		.chart-legend {
+			position: absolute;
+			left: 34px;
+			top: 196px;
+			display: flex;
+			gap: 26px;
+			color: #e8f9ff;
+			font-size: 14px;
+		}
+		.chart-legend i {
+			display: inline-block;
+			width: 10px;
+			height: 10px;
+			margin-right: 7px;
+			border-radius: 2px;
+		}
+		.blue-dot { background: #19aaff; }
+		.yellow-dot { background: #f7e83a; }
+		.pink-dot { background: #ff3aa3; }
+		.purple-dot { background: #756bff; }
+		.capacity-legend {
+			left: 34px;
+			top: 172px;
+			gap: 30px;
+			height: 22px;
+			align-items: center;
+			padding: 2px 8px;
+			background: rgba(5, 58, 140, .36);
+			border: 1px solid rgba(68, 182, 255, .14);
+			font-size: 13px;
+			font-weight: 800;
+			z-index: 4;
+		}
+		.capacity-legend i {
+			width: 9px;
+			height: 9px;
+			margin-right: 6px;
+		}
+		.quality-cards {
+			display: grid;
+			grid-template-columns: 1fr 1fr;
+			gap: 32px;
+			height: 100%;
+			padding: 16px 22px 6px;
+		}
+		.quality-card {
+			position: relative;
+			display: flex;
+			flex-direction: column;
+			align-items: center;
+			justify-content: center;
+			min-width: 0;
+			padding-top: 18px;
+			background: linear-gradient(180deg, transparent 0, rgba(22, 160, 245, .18) 48%, rgba(20, 127, 211, .46) 49%, rgba(11, 86, 174, .32) 100%);
+			clip-path: polygon(16% 0, 84% 0, 100% 34%, 100% 100%, 0 100%, 0 34%);
+		}
+		.quality-icon {
+			position: absolute;
+			top: 2px;
+			left: 50%;
+			transform: translateX(-50%);
+			width: 52px;
+			height: 52px;
+			border-radius: 50%;
+			background: radial-gradient(circle, #1f87df 0, #1562ba 60%, rgba(20, 113, 197, .2) 100%);
+			box-shadow: 0 0 18px rgba(57, 207, 255, .7);
+		}
+		.quality-icon img {
+			position: absolute;
+			left: 10px;
+			top: 10px;
+			width: 32px;
+			height: 32px;
+			object-fit: contain;
+		}
+		.quality-card span {
+			margin-top: 34px;
+			font-size: 15px;
+			font-weight: 800;
+			color: #fff;
+		}
+		.quality-card strong {
+			margin-top: 10px;
+			font-size: 26px;
+			color: #fff;
+			letter-spacing: 1px;
+		}
+		.device-total {
+			display: grid;
+			grid-template-columns: 1fr auto;
+			align-items: center;
+			margin: 8px 4px 14px;
+			padding-bottom: 13px;
+			border-bottom: 1px solid rgba(164, 228, 255, .42);
+			font-size: 18px;
+			font-weight: 900;
+		}
+		.flip-number {
+			display: flex;
+			align-items: center;
+			gap: 7px;
+			margin-right: 5px;
+		}
+		.flip-number span {
+			width: 33px;
+			height: 42px;
+			line-height: 42px;
+			text-align: center;
+			font-size: 31px;
+			font-weight: 900;
+			color: #dfffff;
+			background: linear-gradient(180deg, #1d7cbe, #093c8f);
+			border: 1px solid rgba(110, 224, 255, .34);
+			box-shadow: inset 0 0 13px rgba(83, 221, 255, .38);
+		}
+		.flip-unit {
+			margin-left: 3px;
+			font-size: 14px;
+			color: #c8efff;
+		}
+		.device-split {
+			display: grid;
+			grid-template-columns: repeat(3, 1fr);
+			gap: 0;
+			margin: 0 0 18px;
+		}
+		.device-split div {
+			text-align: center;
+			border-left: 1px solid rgba(133, 224, 255, .25);
+		}
+		.device-split div:first-child { border-left: 0; }
+		.device-split b {
+			display: block;
+			margin-bottom: 5px;
+			color: #ffe83e;
+			font-size: 18px;
+		}
+		.device-split small { font-size: 11px; color: #f9f9ff; }
+		.device-split span { display: block; margin-top: 3px; font-size: 12px; color: #e2f8ff; }
+		.device-subtitle {
+			margin: 0 0 8px;
+			text-align: center;
+			font-size: 15px;
+			font-weight: 900;
+			color: #fff;
+		}
+		.ring-row {
+			display: grid;
+			grid-template-columns: 138px 1fr;
+			gap: 12px;
+			align-items: center;
+		}
+		.ring {
+			width: 130px;
+			height: 130px;
+			border-radius: 50%;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			background: conic-gradient(#3a87ff 0deg, #28d5ff 0deg, rgba(38, 112, 214, .45) 0deg);
+			box-shadow: 0 0 20px rgba(49, 213, 255, .48);
+		}
+		.ring span {
+			width: 78px;
+			height: 78px;
+			border-radius: 50%;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			background: radial-gradient(circle, #33a7ec, #1261ba);
+			font-size: 24px;
+			font-weight: 900;
+		}
+		.metric-line {
+			margin: 13px 0 19px;
+			color: #e7faff;
+			font-size: 15px;
+			font-weight: 900;
+		}
+		.metric-line b {
+			float: right;
+			font-size: 25px;
+			color: #19d6ff;
+		}
+		.metric-line .bar {
+			clear: both;
+			height: 11px;
+			margin-top: 7px;
+			background: rgba(14, 70, 157, .86);
+		}
+		.metric-line .bar span {
+			display: block;
+			width: 0;
+			height: 100%;
+			background: linear-gradient(90deg, #0d8cff, #23f0ff);
+		}
+		.energy-grid {
+			display: grid;
+			grid-template-columns: 1fr 1fr;
+			gap: 22px 10px;
+			padding: 28px 0 0;
+		}
+		.energy-card {
+			position: relative;
+			min-height: 68px;
+			padding: 10px 10px 8px 86px;
+			background: linear-gradient(90deg, rgba(24, 138, 230, .45), rgba(17, 103, 196, .2));
+			clip-path: polygon(0 0, 90% 0, 100% 50%, 90% 100%, 0 100%);
+		}
+		.energy-card img {
+			position: absolute;
+			left: 12px;
+			top: -4px;
+			width: 72px;
+			height: 72px;
+			object-fit: contain;
+		}
+		.energy-card span {
+			display: block;
+			color: #d5f7ff;
+			font-size: 15px;
+			font-weight: 900;
+			line-height: 20px;
+		}
+		.energy-card strong {
+			display: block;
+			margin-top: 2px;
+			font-size: 22px;
+			line-height: 25px;
+			color: #fff;
+			letter-spacing: 1px;
+			overflow-wrap: anywhere;
+		}
+		.data-table {
+			width: 100%;
+			border-collapse: collapse;
+			table-layout: fixed;
+			font-size: 14px;
+		}
+		.data-table th {
+			height: 32px;
+			color: #4ec5ff;
+			font-weight: 900;
+			text-align: center;
+			border-bottom: 1px solid rgba(104, 208, 255, .18);
+		}
+		.data-table td {
+			height: 36px;
+			color: #fff;
+			text-align: center;
+			font-weight: 800;
+			white-space: nowrap;
+			overflow: hidden;
+			text-overflow: ellipsis;
+		}
+		.top-list {
+			position: absolute;
+			left: 248px;
+			top: 70px;
+			right: 20px;
+			bottom: 22px;
+			display: flex;
+			flex-direction: column;
+			justify-content: center;
+			gap: 12px;
+		}
+		.top-item {
+			display: grid;
+			grid-template-columns: 16px 1fr 48px;
+			align-items: center;
+			gap: 8px;
+			color: #e7fbff;
+			font-size: 14px;
+			font-weight: 900;
+		}
+		.top-color {
+			width: 8px;
+			height: 8px;
+			background: #17d9ff;
+		}
+		.top-value { color: #fff; }
+		.warehouse-cards {
+			display: grid;
+			grid-template-columns: 1fr 1fr;
+			gap: 20px;
+			height: 100%;
+			padding: 18px 10px 8px;
+		}
+		.warehouse-card {
+			display: grid;
+			grid-template-columns: 74px 1fr;
+			align-items: center;
+			min-width: 0;
+		}
+		.warehouse-icon {
+			width: 64px;
+			height: 64px;
+			background: url("${ctxStatic}/screen/imgs/screenzong18.png") center / contain no-repeat;
+		}
+		.warehouse-name {
+			font-size: 15px;
+			font-weight: 900;
+			color: #fff;
+			margin-bottom: 10px;
+		}
+		.warehouse-text {
+			font-size: 13px;
+			line-height: 25px;
+			font-weight: 800;
+			color: #1defff;
+			white-space: nowrap;
+		}
+		.empty {
+			height: 170px;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			color: rgba(210, 245, 255, .58);
+			font-size: 15px;
+		}
+	</style>
+</head>
+<body>
+<div class="screen">
+	<div class="header">
+		<div class="logo"></div>
+		<div class="title">MES系统综合数据大屏</div>
+		<div class="clock" id="clock"></div>
+	</div>
+
+	<section class="panel left-top">
+		<div class="panel-title">生产订单信息</div>
+		<div class="panel-body">
+			<div class="kpi-row">
+				<div class="kpi">
+					<img src="${ctxStatic}/screen/imgs/screenzong3.png" alt="">
+					<div><div class="kpi-label">当日产能</div><div class="kpi-value"><span id="todayOutput">0</span></div></div>
+				</div>
+				<div class="kpi">
+					<img src="${ctxStatic}/screen/imgs/screenzong4.png" alt="">
+					<div><div class="kpi-label">日计划产能</div><div class="kpi-value"><span id="dayPlan">0</span></div></div>
+				</div>
+			</div>
+			<div class="progress-wrap">
+				<div class="progress-line"><span id="finishRateBar"></span></div>
+				<div class="progress-meta"><span>已完成产能</span><strong><span id="finishRate">0</span>%</strong></div>
+			</div>
+			<div class="tabs"><span>周</span><span>月</span></div>
+			<div class="chart-legend capacity-legend"><span><i class="blue-dot"></i>完成产量</span><span><i class="yellow-dot"></i>计划产量</span></div>
+			<div id="capacityChart" class="chart"></div>
+		</div>
+	</section>
+
+	<section class="panel left-middle">
+		<div class="panel-title">品质指标记录</div>
+		<div class="panel-body">
+			<div class="quality-cards">
+				<div class="quality-card">
+					<div class="quality-icon"><img src="${ctxStatic}/screen/imgs/screenzong13.png" alt=""></div>
+					<span>巡检合格率</span>
+					<strong><span id="checkPassRate">0</span>%</strong>
+				</div>
+				<div class="quality-card">
+					<div class="quality-icon"><img src="${ctxStatic}/screen/imgs/screenzong14.png" alt=""></div>
+					<span>出货完成率</span>
+					<strong><span id="shipRate">0</span>%</strong>
+				</div>
+			</div>
+		</div>
+	</section>
+
+	<section class="panel left-bottom">
+		<div class="panel-title">每小时上下线数据分析</div>
+		<div class="panel-body">
+			<div class="chart-legend" style="top:54px;left:178px;"><span><i class="pink-dot"></i>上线</span><span><i class="purple-dot"></i>下线</span></div>
+			<div id="hourChart" class="chart"></div>
+		</div>
+	</section>
+
+	<section class="panel right-top">
+		<div class="panel-title">设备在线状态</div>
+		<div class="panel-body">
+			<div class="device-total">
+				<span>设备总数</span>
+				<div class="flip-number" id="deviceDigits"><span>0</span><span>0</span><span>0</span><em class="flip-unit">台</em></div>
+			</div>
+			<div class="device-split">
+				<div><b id="onlineDevices">0 <small>台</small></b><span>运行设备</span></div>
+				<div><b id="idleDevices">0 <small>台</small></b><span>空闲设备</span></div>
+				<div><b id="offlineDevices">0 <small>台</small></b><span>离线设备</span></div>
+			</div>
+			<div class="device-subtitle">智能设备</div>
+			<div class="ring-row">
+				<div>
+					<div class="ring" id="onlineRing"><span id="onlineRate">0%</span></div>
+					<div style="text-align:center;margin-top:4px;font-weight:900;">OEE</div>
+				</div>
+				<div>
+					<div class="metric-line">性能稼动率 <b id="performanceRate">0%</b><div class="bar"><span id="onlineRateBar"></span></div></div>
+					<div class="metric-line">时间开动率 <b id="timeRate">0%</b><div class="bar"><span id="planRateBar"></span></div></div>
+				</div>
+			</div>
+		</div>
+	</section>
+
+	<section class="panel right-middle">
+		<div class="panel-title">绿色制造</div>
+		<div class="panel-body">
+			<div class="energy-grid">
+				<div class="energy-card"><img src="${ctxStatic}/screen/imgs/screenzong15.png" alt=""><span>今日用电/kWh</span><strong><span id="greenEnergy">0</span></strong></div>
+				<div class="energy-card"><img src="${ctxStatic}/screen/imgs/screenzong16.png" alt=""><span>当前总功率/kW</span><strong><span id="greenPower">0</span></strong></div>
+				<div class="energy-card"><img src="${ctxStatic}/screen/imgs/screenzong1.png" alt=""><span id="meterName0">电表一</span><strong><span id="meterEnergy0">0</span></strong></div>
+				<div class="energy-card"><img src="${ctxStatic}/screen/imgs/screenzong2.png" alt=""><span id="meterName1">电表二</span><strong><span id="meterEnergy1">0</span></strong></div>
+			</div>
+		</div>
+	</section>
+
+	<section class="panel right-bottom">
+		<div class="panel-title">仓库出库</div>
+		<div class="panel-body">
+			<div class="warehouse-cards">
+				<div class="warehouse-card">
+					<div class="warehouse-icon"></div>
+					<div>
+						<div class="warehouse-name">今日出库</div>
+						<div class="warehouse-text">数量 <span id="todayOut">0</span> 件</div>
+					</div>
+				</div>
+				<div class="warehouse-card">
+					<div class="warehouse-icon"></div>
+					<div>
+						<div class="warehouse-name">本月出库</div>
+						<div class="warehouse-text">数量 <span id="monthOut">0</span> 件</div>
+					</div>
+				</div>
+			</div>
+		</div>
+	</section>
+
+	<section class="panel bottom-left">
+		<div class="panel-title">制程不良统计TOP5</div>
+		<div class="panel-body">
+			<div id="repairTopChart" class="chart"></div>
+			<div class="top-list" id="repairTopList"></div>
+		</div>
+	</section>
+
+	<section class="panel bottom-right">
+		<div class="panel-title">生产订单明细记录</div>
+		<div class="panel-body">
+			<table class="data-table">
+				<thead><tr><th>品名</th><th>工单号</th><th>工单数量</th><th>完成量</th><th>完成比例</th><th>车间</th></tr></thead>
+				<tbody id="orderRows"></tbody>
+			</table>
+			<div class="empty" id="orderEmpty">暂无生产订单明细</div>
+		</div>
+	</section>
+</div>
+
+<script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
+<script src="${ctxStatic}/echarts/4.2/echarts.min.js"></script>
+<script>
+	var charts = {};
+
+	$(function () {
+		changeZoom();
+		updateClock();
+		getData();
+		setInterval(updateClock, 1000);
+		setInterval(getData, 1000 * 60 * 2);
+	});
+
+	$(window).on('resize', function () {
+		changeZoom();
+		Object.keys(charts).forEach(function (key) {
+			charts[key].resize();
+		});
+	});
+
+	function changeZoom() {
+		var scaleX = $(window).width() / 1920;
+		var scaleY = $(window).height() / 1080;
+		$('body').css('transform', 'scale(' + scaleX + ', ' + scaleY + ')');
+	}
+
+	function updateClock() {
+		var now = new Date();
+		var weeks = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
+		$('#clock').text(now.getFullYear() + '年' + (now.getMonth() + 1) + '月' + now.getDate() + '日 ' + weeks[now.getDay()] + ' ' + pad(now.getHours()) + ':' + pad(now.getMinutes()));
+	}
+
+	function pad(value) {
+		return value < 10 ? '0' + value : value;
+	}
+
+	function getData() {
+		$.post('${ctx}/mes/mesProduct/integratedScreenData', function (ret) {
+			if (!ret || !ret.data) {
+				return;
+			}
+			renderScreen(ret.data);
+		});
+	}
+
+	function renderScreen(data) {
+		var todayOutput = number(data.todayOutput == null ? data.actual : data.todayOutput);
+		var dayPlan = number(data.plan);
+		var finishRate = dayPlan > 0 ? Math.min(100, Math.round(todayOutput * 1000 / dayPlan) / 10) : 0;
+		var deviceTotal = number(data.deviceTotal || data.devices);
+		var onlineDevices = number(data.onlineDevices);
+		var offlineDevices = number(data.offlineDevices);
+		var idleDevices = Math.max(deviceTotal - onlineDevices - offlineDevices, 0);
+		var onlineRate = deviceTotal > 0 ? Math.round(onlineDevices * 1000 / deviceTotal) / 10 : 0;
+		var todayOut = number(data.todayOut);
+		var monthOut = number(data.monthOut);
+		var shipRate = todayOutput > 0 ? Math.min(100, Math.round(todayOut * 1000 / todayOutput) / 10) : 0;
+
+		$('#todayOutput').text(todayOutput);
+		$('#dayPlan').text(dayPlan);
+		$('#finishRate').text(finishRate);
+		$('#finishRateBar').css('width', finishRate + '%');
+		$('#checkPassRate').text(number(data.checkPassRate));
+		$('#shipRate').text(shipRate);
+		$('#todayOut').text(todayOut);
+		$('#monthOut').text(monthOut);
+
+		renderDigits(deviceTotal);
+		$('#onlineDevices').html(onlineDevices + ' <small>台</small>');
+		$('#idleDevices').html(idleDevices + ' <small>台</small>');
+		$('#offlineDevices').html(offlineDevices + ' <small>台</small>');
+		$('#onlineRate').text(onlineRate + '%');
+		$('#performanceRate').text(onlineRate + '%');
+		$('#timeRate').text(finishRate + '%');
+		$('#onlineRateBar').css('width', Math.min(100, onlineRate) + '%');
+		$('#planRateBar').css('width', finishRate + '%');
+		$('#onlineRing').css('background', 'conic-gradient(#3a87ff 0deg, #28d5ff ' + (onlineRate * 3.6) + 'deg, rgba(38, 112, 214, .45) ' + (onlineRate * 3.6) + 'deg)');
+
+		renderOrderRows(data.orderDetails || [], dayPlan);
+		renderEnergy(data.greenMeters || [], data);
+		renderCapacity(data.date || [], data.output || [], data.plan);
+		renderHour(data.hourTimes || [], data.onlineHourlyData || [], data.offlineHourlyData || []);
+		renderRepairTop(data.repairTopNames || [], data.repairTopCounts || []);
+	}
+
+	function number(value) {
+		var n = Number(value);
+		return isNaN(n) ? 0 : n;
+	}
+
+	function text(value) {
+		return value === undefined || value === null || value === '' ? '-' : String(value);
+	}
+
+	function renderDigits(value) {
+		var digits = String(Math.max(0, number(value))).padStart(3, '0').slice(-3).split('');
+		$('#deviceDigits').html('<span>' + digits[0] + '</span><span>' + digits[1] + '</span><span>' + digits[2] + '</span><em class="flip-unit">台</em>');
+	}
+
+	function renderOrderRows(rows, plan) {
+		if (!rows.length) {
+			$('#orderRows').html('');
+			$('#orderEmpty').show();
+			return;
+		}
+		$('#orderEmpty').hide();
+		var html = '';
+		rows.slice(0, 5).forEach(function (row, index) {
+			var done = number(row.done);
+			var orderNum = number(row.orderNum || plan || done);
+			var rate = orderNum > 0 ? Math.min(100, Math.round(done * 10000 / orderNum) / 100) : 0;
+			html += '<tr><td>' + text(row.name || ('品名' + (index + 1))) + '</td><td>' + text(row.orderNo) + '</td><td>' + orderNum + '</td><td>' + done + '</td><td>' + rate + '%</td><td>' + text(row.workshop || 'SEB车间') + '</td></tr>';
+		});
+		$('#orderRows').html(html);
+	}
+
+	function renderEnergy(meters, data) {
+		for (var i = 0; i < 2; i++) {
+			var meter = meters[i] || {};
+			$('#meterName' + i).text(text(meter.deviceName || meter.deviceSn || ('电表' + (i + 1))));
+			$('#meterEnergy' + i).text(formatNumber(number(meter.todayEnergy)) + ' kWh');
+		}
+		$('#greenPower').text(formatNumber(number(data.greenTotalPower)));
+		$('#greenEnergy').text(formatNumber(number(data.greenTodayEnergy)));
+	}
+
+	function chart(id) {
+		if (!charts[id]) {
+			charts[id] = echarts.init(document.getElementById(id));
+		}
+		return charts[id];
+	}
+
+	function renderCapacity(dates, output, plan) {
+		var labels = (dates || []).slice(-7);
+		var values = (output || []).slice(-7).map(number);
+		var planValue = number(plan);
+		chart('capacityChart').setOption({
+			color: ['#19aaff', '#f7e83a'],
+			tooltip: { trigger: 'axis' },
+			grid: { left: 34, right: 8, top: 18, bottom: 24 },
+			xAxis: { type: 'category', data: labels, axisLabel: { color: '#fff', fontSize: 11 }, axisLine: { lineStyle: { color: '#2479b5' } }, axisTick: { show: false } },
+			yAxis: { type: 'value', axisLabel: { color: '#fff', fontSize: 11 }, splitLine: { show: false } },
+			series: [
+				{ name: '完成产量', type: 'bar', barWidth: 12, data: values, itemStyle: { barBorderRadius: [6, 6, 0, 0] } },
+				{ name: '计划产量', type: 'bar', barWidth: 12, data: labels.map(function () { return planValue; }), itemStyle: { barBorderRadius: [6, 6, 0, 0] } }
+			]
+		}, true);
+	}
+
+	function renderHour(hours, onlineValues, offlineValues) {
+		chart('hourChart').setOption({
+			color: ['#ff3aa3', '#756bff'],
+			tooltip: { trigger: 'axis' },
+			grid: { left: 36, right: 10, top: 48, bottom: 28 },
+			xAxis: { type: 'category', data: hours, axisLabel: { color: '#fff', fontSize: 11 }, axisLine: { lineStyle: { color: '#2479b5' } }, axisTick: { show: false } },
+			yAxis: { type: 'value', max: 60, minInterval: 1, axisLabel: { color: '#fff', fontSize: 11, formatter: function (value) { return Math.floor(value) === value ? value : ''; } }, splitLine: { lineStyle: { color: 'rgba(128, 223, 255, .12)' } } },
+			series: [
+				{ name: '上线', type: 'line', smooth: true, symbol: 'none', lineStyle: { width: 3 }, areaStyle: { color: 'rgba(255, 58, 163, .18)' }, data: (onlineValues || []).map(number) },
+				{ name: '下线', type: 'line', smooth: true, symbol: 'none', lineStyle: { width: 3 }, areaStyle: { color: 'rgba(117, 107, 255, .24)' }, data: (offlineValues || []).map(number) }
+			]
+		}, true);
+	}
+
+	function renderRepairTop(names, counts) {
+		var colors = ['#25e8ff', '#ff6a2a', '#ffbf2d', '#2d92ff', '#18e5d8'];
+		if (!names.length) {
+			names = ['暂无不良记录'];
+			counts = [0];
+		}
+		var data = names.slice(0, 5).map(function (name, index) {
+			return { name: name, value: number(counts[index]), itemStyle: { color: colors[index % colors.length] } };
+		});
+		chart('repairTopChart').setOption({
+			tooltip: { trigger: 'item' },
+			series: [{
+				type: 'pie',
+				radius: ['58%', '78%'],
+				center: ['50%', '50%'],
+				avoidLabelOverlap: true,
+				label: { show: false },
+				labelLine: { show: false },
+				data: data
+			}]
+		}, true);
+		var html = '';
+		data.forEach(function (item, index) {
+			html += '<div class="top-item"><i class="top-color" style="background:' + colors[index % colors.length] + '"></i><span>' + text(item.name) + '</span><span class="top-value">' + item.value + '</span></div>';
+		});
+		$('#repairTopList').html(html);
+	}
+
+	function formatNumber(value) {
+		var n = number(value);
+		return Math.round(n * 100) / 100;
+	}
+</script>
+</body>
+</html>