Procházet zdrojové kódy

增加设备、物料、质量管理大屏

jingbo před 4 měsíci
rodič
revize
bb40195b5b

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

@@ -1,17 +1,21 @@
 package com.jeesite.modules.mes.entity;
 
-import com.jeesite.common.entity.TreeEntity;
-import com.jeesite.common.mybatis.annotation.Column;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+import java.util.Date;
 import com.jeesite.common.mybatis.annotation.JoinTable;
 import com.jeesite.common.mybatis.annotation.JoinTable.Type;
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.entity.TreeEntity;
+import com.jeesite.common.mybatis.annotation.Column;
 import com.jeesite.common.mybatis.annotation.Table;
 import com.jeesite.common.mybatis.mapper.query.QueryType;
 import com.jeesite.common.utils.excel.annotation.ExcelField;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Align;
 import com.jeesite.common.utils.excel.annotation.ExcelFields;
 
-import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.Size;
-
 /**
  * 设备Entity
  * @author mes
@@ -41,6 +45,7 @@ import javax.validation.constraints.Size;
 		@Column(name="interval", attrName="interval", label="生成时间(秒)"),
 		@Column(name="product_sn", attrName="productSn", label="当前工件"),
 		@Column(name="device_code", attrName="deviceCode", label="设备识别码"),
+		@Column(name="image_path", attrName="imagePath", label="设备图片", isQuery=false),
 
 }, joinTable={
 		@JoinTable(type=Type.LEFT_JOIN, entity=MesDeviceCate.class, attrName="this", alias="b",
@@ -75,18 +80,8 @@ public class MesDevice extends TreeEntity<MesDevice> {
 
 	private String lineSn;
 	private String deviceCode;
-	@ExcelFields({
-			@ExcelField(title="设备名称", attrName="treeName", align= ExcelField.Align.CENTER, sort=20),
-			@ExcelField(title="唯一编号", attrName="sn", align= ExcelField.Align.CENTER, sort=30),
-			@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),
-			@ExcelField(title="工位", attrName="gw", align= ExcelField.Align.CENTER, sort=80),
-			@ExcelField(title="设备工作状态", attrName="workStatus",dictType="mes_device_work_status", align= ExcelField.Align.CENTER, sort=90),
-			@ExcelField(title="状态", attrName="workStatus",dictType="mes_device_work_status", align= ExcelField.Align.CENTER, sort=100),
-			@ExcelField(title="备注", attrName="remark", align= ExcelField.Align.CENTER, sort=110),
-	})
+	private String imagePath;
+
 	public String getPostion() {
 		return postion;
 	}
@@ -160,11 +155,11 @@ public class MesDevice extends TreeEntity<MesDevice> {
 	}
 
 
-/*	@ExcelFields({
+	@ExcelFields({
 		@ExcelField(title="规格型号", attrName="spec", align=Align.CENTER, sort=30),
 		@ExcelField(title="制造商", attrName="company", align=Align.CENTER, sort=40),
 		@ExcelField(title="出场日期", attrName="outdate", align=Align.CENTER, sort=50, dataFormat="yyyy-MM-dd"),
-	})*/
+	})
 	public MesDevice() {
 		this(null);
 	}
@@ -285,4 +280,12 @@ public class MesDevice extends TreeEntity<MesDevice> {
 	public void setDeviceCode(String deviceCode) {
 		this.deviceCode = deviceCode;
 	}
-}
+
+	public String getImagePath() {
+		return imagePath;
+	}
+
+	public void setImagePath(String imagePath) {
+		this.imagePath = imagePath;
+	}
+}

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

@@ -161,6 +161,22 @@ public class MesDeviceUrgency extends DataEntity<MesDeviceUrgency> {
 	public void setEndDate(Date endDate) {
 		this.endDate = endDate;
 	}
+
+	public Date getStartDate_gte() {
+		return sqlMap.getWhere().getValue("start_date", QueryType.GTE);
+	}
+
+	public void setStartDate_gte(Date startDate) {
+		sqlMap.getWhere().and("start_date", QueryType.GTE, startDate);
+	}
+
+	public Date getStartDate_lte() {
+		return sqlMap.getWhere().getValue("start_date", QueryType.LTE);
+	}
+
+	public void setStartDate_lte(Date startDate) {
+		sqlMap.getWhere().and("start_date", QueryType.LTE, startDate);
+	}
 	
 	@NotBlank(message="状态 0=处理中 1=已完成不能为空")
 	@Size(min=0, max=1, message="状态 0=处理中 1=已完成长度不能超过 1 个字符")
@@ -252,4 +268,4 @@ public class MesDeviceUrgency extends DataEntity<MesDeviceUrgency> {
 	public void setMesDeviceConsumableRecordList(List<MesDeviceConsumableRecord> mesDeviceConsumableRecordList) {
 		this.mesDeviceConsumableRecordList = mesDeviceConsumableRecordList;
 	}
-}
+}

+ 5 - 1
src/main/java/com/jeesite/modules/mes/service/MesDeviceStateService.java

@@ -63,6 +63,10 @@ public class MesDeviceStateService extends CrudService<MesDeviceStateDao, MesDev
 	public List<MesDeviceState> findList(MesDeviceState mesDeviceState) {
 		return super.findList(mesDeviceState);
 	}
+
+	public MesDeviceState findInfo(MesDeviceState mesDeviceState) {
+		return mesDeviceStateDao.findInfo(mesDeviceState);
+	}
 	
 	/**
 	 * 保存数据(插入或更新)
@@ -189,4 +193,4 @@ public class MesDeviceStateService extends CrudService<MesDeviceStateDao, MesDev
 		super.save(mesDeviceState);
 	}
 	
-}
+}

+ 369 - 1
src/main/java/com/jeesite/modules/mes/web/MesDeviceController.java

@@ -1,7 +1,11 @@
 package com.jeesite.modules.mes.web;
 
+import java.text.SimpleDateFormat;
 import java.util.Map;
 import java.util.List;
+import java.util.Date;
+import java.util.Calendar;
+import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
@@ -10,9 +14,15 @@ import com.jeesite.common.entity.Page;
 import com.jeesite.common.lang.ObjectUtils;
 import com.jeesite.modules.mes.entity.MesDeviceCate;
 import com.jeesite.modules.mes.entity.MesDeviceForm;
+import com.jeesite.modules.mes.entity.MesDevicePreventTask;
+import com.jeesite.modules.mes.entity.MesDeviceState;
+import com.jeesite.modules.mes.entity.MesDeviceUrgency;
 import com.jeesite.modules.mes.entity.MesLineProcess;
 import com.jeesite.modules.mes.resp.CommonResp;
 import com.jeesite.modules.mes.service.MesDeviceCateService;
+import com.jeesite.modules.mes.service.MesDevicePreventTaskService;
+import com.jeesite.modules.mes.service.MesDeviceStateService;
+import com.jeesite.modules.mes.service.MesDeviceUrgencyService;
 import org.apache.shiro.authz.annotation.RequiresPermissions;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
@@ -52,6 +62,15 @@ public class MesDeviceController extends BaseController {
 	@Autowired
 	private MesDeviceCateService mesDeviceCateService;
 
+	@Autowired
+	private MesDevicePreventTaskService mesDevicePreventTaskService;
+
+	@Autowired
+	private MesDeviceStateService mesDeviceStateService;
+
+	@Autowired
+	private MesDeviceUrgencyService mesDeviceUrgencyService;
+
 	/**
 	 * 获取数据
 	 */
@@ -390,4 +409,353 @@ public class MesDeviceController extends BaseController {
 		return "modules/mes/equipment/equipment_map";
 	}
 
-}
+	@RequestMapping(value = "oeeScreen")
+	public String oeeScreen(Model model) {
+		return "modules/mes/mesDeviceOeeScreen";
+	}
+
+	@RequestMapping(value = "screen5Data")
+	@ResponseBody
+	public CommonResp<Map<String, Object>> screen5Data() {
+		CommonResp<Map<String, Object>> resp = new CommonResp<>();
+		Map<String, Object> data = MapUtils.newHashMap();
+		Date now = new Date();
+		Date weekStart = DateUtils.addDays(now, -7);
+
+		data.put("deviceCount", mesDeviceService.findList(new MesDevice()).size());
+
+		MesDevicePreventTask preventQuery = new MesDevicePreventTask();
+		preventQuery.setStartDate_gte(weekStart);
+		preventQuery.setStartDate_lte(now);
+		List<MesDevicePreventTask> preventList = mesDevicePreventTaskService.findList(preventQuery);
+		data.put("preventiveCount", preventList.size());
+
+		List<Map<String, Object>> preventiveStatus = ListUtils.newArrayList();
+		long pending = preventList.stream().filter(p -> "0".equals(p.getState())).count();
+		long processing = preventList.stream().filter(p -> "1".equals(p.getState())).count();
+		long completed = preventList.stream().filter(p -> "2".equals(p.getState())).count();
+		long unfinished = preventList.stream().filter(p -> "3".equals(p.getState())).count();
+
+		Map<String, Object> pendingMap = MapUtils.newHashMap();
+		pendingMap.put("name", "未开始");
+		pendingMap.put("value", pending);
+		preventiveStatus.add(pendingMap);
+
+		Map<String, Object> processingMap = MapUtils.newHashMap();
+		processingMap.put("name", "进行中");
+		processingMap.put("value", processing);
+		preventiveStatus.add(processingMap);
+
+		Map<String, Object> completedMap = MapUtils.newHashMap();
+		completedMap.put("name", "已完成");
+		completedMap.put("value", completed);
+		preventiveStatus.add(completedMap);
+
+		Map<String, Object> unfinishedMap = MapUtils.newHashMap();
+		unfinishedMap.put("name", "未完成");
+		unfinishedMap.put("value", unfinished);
+		preventiveStatus.add(unfinishedMap);
+
+		data.put("preventiveStatus", preventiveStatus);
+		data.put("preventiveList", preventList.stream().limit(10).collect(Collectors.toList()));
+
+		MesDeviceUrgency urgencyQuery = new MesDeviceUrgency();
+		urgencyQuery.setStartDate_gte(weekStart);
+		urgencyQuery.setStartDate_lte(now);
+		List<MesDeviceUrgency> urgencyList = mesDeviceUrgencyService.findList(urgencyQuery);
+		data.put("urgencyCount", urgencyList.size());
+
+		List<Map<String, Object>> urgencyStatus = ListUtils.newArrayList();
+		long urgencyPending = urgencyList.stream().filter(u -> "0".equals(u.getState())).count();
+		long urgencyCompleted = urgencyList.stream().filter(u -> "1".equals(u.getState())).count();
+
+		Map<String, Object> urgencyPendingMap = MapUtils.newHashMap();
+		urgencyPendingMap.put("name", "维修中");
+		urgencyPendingMap.put("value", urgencyPending);
+		urgencyPendingMap.put("color", "#ff9800");
+		urgencyStatus.add(urgencyPendingMap);
+
+		Map<String, Object> urgencyCompletedMap = MapUtils.newHashMap();
+		urgencyCompletedMap.put("name", "已完成");
+		urgencyCompletedMap.put("value", urgencyCompleted);
+		urgencyCompletedMap.put("color", "#00d5d8");
+		urgencyStatus.add(urgencyCompletedMap);
+
+		data.put("urgencyStatus", urgencyStatus);
+
+		Map<String, Long> contentCount = urgencyList.stream()
+				.filter(u -> StringUtils.isNotBlank(u.getContent()))
+				.collect(Collectors.groupingBy(u -> u.getContent(), Collectors.counting()));
+		List<Map<String, Object>> urgencyContent = ListUtils.newArrayList();
+		contentCount.entrySet().stream()
+				.sorted((a, b) -> Long.compare(b.getValue(), a.getValue()))
+				.limit(6)
+				.forEach(e -> {
+					Map<String, Object> contentMap = MapUtils.newHashMap();
+					contentMap.put("name", e.getKey());
+					contentMap.put("value", e.getValue());
+					urgencyContent.add(contentMap);
+				});
+		data.put("urgencyContent", urgencyContent);
+		data.put("urgencyList", urgencyList.stream().limit(10).collect(Collectors.toList()));
+		data.put("weekStart", DateUtils.formatDate(weekStart, "yyyy-MM-dd HH:mm:ss"));
+		data.put("weekEnd", DateUtils.formatDate(now, "yyyy-MM-dd HH:mm:ss"));
+
+		resp.setData(data);
+		resp.setResult(Global.TRUE);
+		return resp;
+	}
+
+	@RequestMapping(value = "oeeScreenData")
+	@ResponseBody
+	public CommonResp<Map<String, Object>> oeeScreenData() {
+		CommonResp<Map<String, Object>> resp = new CommonResp<>();
+		Map<String, Object> data = MapUtils.newHashMap();
+		Date now = new Date();
+		Calendar calendar = Calendar.getInstance();
+		calendar.set(Calendar.HOUR_OF_DAY, 0);
+		calendar.set(Calendar.MINUTE, 0);
+		calendar.set(Calendar.SECOND, 0);
+		calendar.set(Calendar.MILLISECOND, 0);
+		Date todayStart = calendar.getTime();
+		long elapsedSeconds = Math.max(1L, (now.getTime() - todayStart.getTime()) / 1000);
+
+		List<MesDevice> devices = mesDeviceService.findList(new MesDevice());
+		List<Map<String, Object>> deviceCards = ListUtils.newArrayList();
+		List<Map<String, Object>> ranking = ListUtils.newArrayList();
+		long runningCount = 0;
+		long faultCount = 0;
+		long maintenanceCount = 0;
+		long alarmCount = 0;
+		long offlineCount = 0;
+		long unknownCount = 0;
+		long totalRunningSeconds = 0;
+
+		for (int i = 0; i < devices.size(); i++) {
+			MesDevice device = devices.get(i);
+			MesDeviceState latestQuery = new MesDeviceState();
+			latestQuery.setDeviceSn(device.getSn());
+			MesDeviceState latestState = StringUtils.isBlank(device.getSn()) ? null : mesDeviceStateService.findInfo(latestQuery);
+			String state = latestState != null && StringUtils.isNotBlank(latestState.getState()) ? latestState.getState() : device.getWorkStatus();
+			String statusCode = getDeviceScreenStatus(device, state);
+			if ("running".equals(statusCode)) {
+				runningCount++;
+			} else if ("fault".equals(statusCode)) {
+				faultCount++;
+			} else if ("maintenance".equals(statusCode)) {
+				maintenanceCount++;
+			} else if ("alarm".equals(statusCode)) {
+				alarmCount++;
+			} else if ("offline".equals(statusCode)) {
+				offlineCount++;
+			} else {
+				unknownCount++;
+			}
+
+			MesDeviceState stateQuery = new MesDeviceState();
+			stateQuery.setDeviceSn(device.getSn());
+			stateQuery.setStartDate_gte(todayStart);
+			stateQuery.setStartDate_lte(now);
+			List<MesDeviceState> stateList = StringUtils.isBlank(device.getSn()) ? ListUtils.newArrayList() : mesDeviceStateService.findList(stateQuery);
+			long runningSeconds = getRunningSeconds(stateList, todayStart, now);
+			if (isRunningState(state) && latestState != null && latestState.getStartDate() != null && latestState.getStartDate().before(todayStart)) {
+				runningSeconds = elapsedSeconds;
+			}
+			if (runningSeconds <= 0 && isRunningState(state)) {
+				runningSeconds = getRunningSecondsFromWorkTime(device.getWorkTime(), todayStart, now);
+			}
+			totalRunningSeconds += runningSeconds;
+			double openRate = Math.min(100D, runningSeconds * 100D / elapsedSeconds);
+
+			Map<String, Object> card = MapUtils.newHashMap();
+			card.put("name", getText(device.getTreeName(), getText(device.getSn(), "-")));
+			card.put("sn", getText(device.getSn(), "-"));
+			card.put("deviceCode", getText(device.getDeviceCode(), "-"));
+			card.put("lineSn", getText(device.getLineSn(), "-"));
+			card.put("imagePath", getFirstImagePath(device.getImagePath()));
+			card.put("statusCode", statusCode);
+			card.put("statusName", getDeviceScreenStatusName(statusCode));
+			card.put("stateName", getDeviceStateName(state));
+			card.put("openRate", Math.round(openRate * 10D) / 10D);
+			card.put("openMinutes", Math.round(runningSeconds / 60D));
+			card.put("lastReportTime", latestState != null && latestState.getStartDate() != null ? DateUtils.formatDate(latestState.getStartDate(), "HH:mm:ss") : "-");
+			card.put("lastReportDate", latestState != null && latestState.getStartDate() != null ? DateUtils.formatDate(latestState.getStartDate(), "yyyy-MM-dd HH:mm:ss") : "-");
+			card.put("imageIndex", i % 8 + 1);
+			deviceCards.add(card);
+
+			Map<String, Object> rankItem = MapUtils.newHashMap();
+			rankItem.put("name", card.get("name"));
+			rankItem.put("sn", card.get("sn"));
+			rankItem.put("openRate", card.get("openRate"));
+			ranking.add(rankItem);
+		}
+
+		ranking.sort((a, b) -> Double.compare(Double.valueOf(String.valueOf(b.get("openRate"))), Double.valueOf(String.valueOf(a.get("openRate")))));
+		if (ranking.size() > 10) {
+			ranking = ranking.subList(0, 10);
+		}
+
+		List<Map<String, Object>> statusList = ListUtils.newArrayList();
+		addStatusItem(statusList, "运行", runningCount, "#35f2c2");
+		addStatusItem(statusList, "故障", faultCount, "#ff5f6d");
+		addStatusItem(statusList, "维修", maintenanceCount, "#ffc857");
+		addStatusItem(statusList, "报警", alarmCount, "#ff8a3d");
+		addStatusItem(statusList, "离线", offlineCount, "#7d8da8");
+		addStatusItem(statusList, "未知", unknownCount, "#49a7ff");
+
+		double avgOpenRate = devices.isEmpty() ? 0D : totalRunningSeconds * 100D / (elapsedSeconds * devices.size());
+		data.put("deviceTotal", devices.size());
+		data.put("runningCount", runningCount);
+		data.put("faultCount", faultCount);
+		data.put("offlineCount", offlineCount);
+		data.put("avgOpenRate", Math.round(avgOpenRate * 10D) / 10D);
+		data.put("statusList", statusList);
+		data.put("ranking", ranking);
+		data.put("devices", deviceCards);
+		data.put("statDate", DateUtils.formatDate(now, "yyyy-MM-dd"));
+		data.put("statTime", DateUtils.formatDate(now, "HH:mm:ss"));
+		data.put("dataNote", "当前系统缺少计划产量、良品数、理想节拍,页面展示的是基于设备状态记录计算的开动率。");
+		resp.setData(data);
+		resp.setResult(Global.TRUE);
+		return resp;
+	}
+
+	private long getRunningSeconds(List<MesDeviceState> stateList, Date todayStart, Date now) {
+		long runningSeconds = 0L;
+		for (MesDeviceState state : stateList) {
+			if (!isRunningState(state.getState()) || state.getStartDate() == null) {
+				continue;
+			}
+			Date start = state.getStartDate().before(todayStart) ? todayStart : state.getStartDate();
+			Date end = state.getEndDate();
+			if (end == null && state.getInterval() != null) {
+				end = new Date(state.getStartDate().getTime() + state.getInterval() * 1000L);
+			}
+			if (end == null || end.after(now)) {
+				end = now;
+			}
+			if (end.after(start)) {
+				runningSeconds += (end.getTime() - start.getTime()) / 1000;
+			}
+		}
+		return runningSeconds;
+	}
+
+	private long getRunningSecondsFromWorkTime(String workTime, Date todayStart, Date now) {
+		if (StringUtils.isBlank(workTime)) {
+			return 0L;
+		}
+		Date start = parseWorkTime(workTime);
+		if (start == null) {
+			return 0L;
+		}
+		if (start.before(todayStart)) {
+			start = todayStart;
+		}
+		if (!now.after(start)) {
+			return 0L;
+		}
+		return (now.getTime() - start.getTime()) / 1000;
+	}
+
+	private Date parseWorkTime(String workTime) {
+		String[] patterns = {"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm"};
+		for (String pattern : patterns) {
+			try {
+				return new SimpleDateFormat(pattern).parse(workTime);
+			} catch (Exception ignored) {
+			}
+		}
+		return null;
+	}
+
+	private boolean isRunningState(String state) {
+		return "1".equals(state) || "2".equals(state);
+	}
+
+	private String getDeviceScreenStatus(MesDevice device, String state) {
+		if (device.getOnline() != null && device.getOnline() == 1) {
+			return "offline";
+		}
+		if ("3".equals(state)) {
+			return "fault";
+		}
+		if ("4".equals(state)) {
+			return "maintenance";
+		}
+		if ("5".equals(state) || "6".equals(state)) {
+			return "alarm";
+		}
+		if (isRunningState(state)) {
+			return "running";
+		}
+		return "unknown";
+	}
+
+	private String getDeviceScreenStatusName(String statusCode) {
+		if ("running".equals(statusCode)) {
+			return "运行";
+		}
+		if ("fault".equals(statusCode)) {
+			return "故障";
+		}
+		if ("maintenance".equals(statusCode)) {
+			return "维修";
+		}
+		if ("alarm".equals(statusCode)) {
+			return "报警";
+		}
+		if ("offline".equals(statusCode)) {
+			return "离线";
+		}
+		return "未知";
+	}
+
+	private String getDeviceStateName(String state) {
+		if ("1".equals(state)) {
+			return "正常";
+		}
+		if ("2".equals(state)) {
+			return "手动";
+		}
+		if ("3".equals(state)) {
+			return "故障";
+		}
+		if ("4".equals(state)) {
+			return "维修";
+		}
+		if ("5".equals(state)) {
+			return "质量报警";
+		}
+		if ("6".equals(state)) {
+			return "物流堵料";
+		}
+		return "未知";
+	}
+
+	private void addStatusItem(List<Map<String, Object>> list, String name, long value, String color) {
+		Map<String, Object> item = MapUtils.newHashMap();
+		item.put("name", name);
+		item.put("value", value);
+		item.put("color", color);
+		list.add(item);
+	}
+
+	private String getText(String value, String defaultValue) {
+		return StringUtils.isBlank(value) ? defaultValue : value;
+	}
+
+	private String getFirstImagePath(String imagePath) {
+		if (StringUtils.isBlank(imagePath)) {
+			return "";
+		}
+		String[] paths = imagePath.split("\\|");
+		for (String path : paths) {
+			if (StringUtils.isNotBlank(path)) {
+				return path;
+			}
+		}
+		return "";
+	}
+
+}

+ 612 - 90
src/main/java/com/jeesite/modules/mes/web/MesMaterialController.java

@@ -1,36 +1,62 @@
 package com.jeesite.modules.mes.web;
 
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
 import com.alibaba.fastjson.JSONValidator;
 import com.jeesite.common.codec.EncodeUtils;
-import com.jeesite.common.collect.ListUtils;
-import com.jeesite.common.config.Global;
-import com.jeesite.common.entity.Page;
+import com.jeesite.common.entity.DataEntity;
 import com.jeesite.common.lang.DateUtils;
-import com.jeesite.common.service.ServiceException;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.mybatis.mapper.query.QueryType;
 import com.jeesite.common.utils.excel.ExcelExport;
-import com.jeesite.common.utils.excel.ExcelImport;
-import com.jeesite.common.utils.excel.annotation.ExcelField;
-import com.jeesite.common.validator.ValidatorUtils;
-import com.jeesite.common.web.BaseController;
-import com.jeesite.modules.mes.entity.MesMaterial;
-import com.jeesite.modules.mes.service.MesMaterialService;
+import com.jeesite.modules.mes.entity.MesMaterialBfRecord;
+import com.jeesite.modules.mes.entity.MesMaterialBlpRecord;
+import com.jeesite.modules.mes.entity.MesMaterialCheckRecord;
+import com.jeesite.modules.mes.entity.MesMaterialCkLog;
+import com.jeesite.modules.mes.entity.MesMaterialKcLog;
+import com.jeesite.modules.mes.entity.MesMaterialLlRecord;
+import com.jeesite.modules.mes.entity.MesMaterialReport;
+import com.jeesite.modules.mes.entity.MesMaterialRkLog;
+import com.jeesite.modules.mes.entity.MesMaterialRkRecord;
+import com.jeesite.modules.mes.entity.MesMaterialThRecord;
+import com.jeesite.modules.mes.entity.MesNg;
+import com.jeesite.modules.mes.resp.CommonResp;
+import com.jeesite.modules.mes.service.MesMaterialBfRecordService;
+import com.jeesite.modules.mes.service.MesMaterialBlpRecordService;
+import com.jeesite.modules.mes.service.MesMaterialCheckRecordService;
+import com.jeesite.modules.mes.service.MesMaterialCkLogService;
+import com.jeesite.modules.mes.service.MesMaterialKcLogService;
+import com.jeesite.modules.mes.service.MesMaterialLlRecordService;
+import com.jeesite.modules.mes.service.MesMaterialReportService;
+import com.jeesite.modules.mes.service.MesMaterialRkLogService;
+import com.jeesite.modules.mes.service.MesMaterialRkRecordService;
+import com.jeesite.modules.mes.service.MesMaterialThRecordService;
 import org.apache.shiro.authz.annotation.RequiresPermissions;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
-import org.springframework.transaction.annotation.Transactional;
 import org.springframework.ui.Model;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.ModelAttribute;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
-import org.springframework.web.multipart.MultipartFile;
 
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.validation.ConstraintViolation;
-import javax.validation.ConstraintViolationException;
-import java.util.List;
+import com.jeesite.common.config.Global;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.entity.MesMaterial;
+import com.jeesite.modules.mes.service.MesMaterialService;
 
 /**
  * 物料表Controller
@@ -43,6 +69,26 @@ public class MesMaterialController extends BaseController {
 
 	@Autowired
 	private MesMaterialService mesMaterialService;
+	@Autowired
+	private MesMaterialKcLogService mesMaterialKcLogService;
+	@Autowired
+	private MesMaterialRkLogService mesMaterialRkLogService;
+	@Autowired
+	private MesMaterialCkLogService mesMaterialCkLogService;
+	@Autowired
+	private MesMaterialReportService mesMaterialReportService;
+	@Autowired
+	private MesMaterialCheckRecordService mesMaterialCheckRecordService;
+	@Autowired
+	private MesMaterialRkRecordService mesMaterialRkRecordService;
+	@Autowired
+	private MesMaterialLlRecordService mesMaterialLlRecordService;
+	@Autowired
+	private MesMaterialBlpRecordService mesMaterialBlpRecordService;
+	@Autowired
+	private MesMaterialThRecordService mesMaterialThRecordService;
+	@Autowired
+	private MesMaterialBfRecordService mesMaterialBfRecordService;
 	
 	/**
 	 * 获取数据
@@ -133,12 +179,559 @@ public class MesMaterialController extends BaseController {
 	/**
 	 * 导出数据
 	 */
+	@RequestMapping(value = "materialScreen")
+	public String materialScreen(Model model) {
+		return "modules/mes/mesMaterialScreen";
+	}
+
+	@RequestMapping(value = "materialScreenData")
+	@ResponseBody
+	public CommonResp materialScreenData() {
+		CommonResp<Map<String, Object>> resp = new CommonResp<>();
+		Map<String, Object> data = new LinkedHashMap<>();
+		Date now = new Date();
+		Date todayStart = dayStart(now);
+		Date weekStart = dayStart(addDays(now, -6));
+		Date warningDate = addDays(todayStart, 30);
+
+		List<MesMaterial> materialList = mesMaterialService.findList(new MesMaterial());
+		List<MesMaterialKcLog> stockList = mesMaterialKcLogService.findList(new MesMaterialKcLog());
+		List<MesMaterialKcLog> validStockList = filterPositiveStock(stockList);
+		List<MesMaterialRkLog> rkLogList = findRkLogs(weekStart, now);
+		List<MesMaterialCkLog> ckLogList = findCkLogs(weekStart, now);
+		List<MesMaterialReport> reportList = findReports(weekStart, now);
+		List<MesMaterialCheckRecord> checkList = findCheckRecords(weekStart, now);
+		List<MesMaterialRkRecord> rkRecordList = findRkRecords(weekStart, now);
+		List<MesMaterialLlRecord> llRecordList = findLlRecords(weekStart, now);
+		List<MesMaterialBlpRecord> blpRecordList = findBlpRecords(weekStart, now);
+		List<MesMaterialThRecord> thRecordList = findThRecords(weekStart, now);
+		List<MesMaterialBfRecord> bfRecordList = findBfRecords(weekStart, now);
+
+		Map<String, Object> summary = new LinkedHashMap<>();
+		summary.put("materialCount", materialList.size());
+		summary.put("stockBatchCount", validStockList.size());
+		summary.put("stockTotal", sumKc(validStockList));
+		summary.put("todayInbound", sumRk(filterByDate(rkLogList, todayStart, now)));
+		summary.put("todayOutbound", sumCk(filterByDate(ckLogList, todayStart, now)));
+		summary.put("todayReport", filterByDate(reportList, todayStart, now).size());
+		summary.put("pendingReport", countReportState(reportList, "0"));
+		summary.put("todayAbnormal", filterByDate(blpRecordList, todayStart, now).size()
+				+ filterByDate(thRecordList, todayStart, now).size()
+				+ filterByDate(bfRecordList, todayStart, now).size());
+		summary.put("expireWarning", countExpireWarning(validStockList, todayStart, warningDate));
+		data.put("summary", summary);
+
+		data.put("days", buildDayLabels(weekStart));
+		data.put("trend", buildTrend(weekStart, rkLogList, ckLogList, reportList, blpRecordList, thRecordList, bfRecordList));
+		data.put("reportStatus", toChartData(countReports(reportList)));
+		data.put("checkStatus", toChartData(countChecks(checkList)));
+		data.put("blpStatus", toChartData(countBlp(blpRecordList)));
+		data.put("rkType", toChartData(countRkType(rkRecordList)));
+		data.put("llStatus", toChartData(countLl(llRecordList)));
+		data.put("stockByLocation", toChartData(top(countStockByLocation(validStockList), 8)));
+		data.put("stockByMaterial", toChartData(top(countStockByMaterial(validStockList), 8)));
+		data.put("stockList", topStockRows(validStockList, 20));
+		data.put("expireList", expireRows(validStockList, todayStart, warningDate, 12));
+		data.put("latestActions", latestActions(reportList, checkList, rkLogList, ckLogList, blpRecordList, thRecordList, bfRecordList, 14));
+
+		resp.setData(data);
+		resp.setResult(Global.TRUE);
+		return resp;
+	}
+
+	private List<MesMaterialRkLog> findRkLogs(Date start, Date end) {
+		MesMaterialRkLog query = new MesMaterialRkLog();
+		query.getSqlMap().getWhere().and("create_date", QueryType.GTE, start);
+		query.getSqlMap().getWhere().and("create_date", QueryType.LTE, end);
+		return mesMaterialRkLogService.findList(query);
+	}
+
+	private List<MesMaterialCkLog> findCkLogs(Date start, Date end) {
+		MesMaterialCkLog query = new MesMaterialCkLog();
+		query.setCreateDate_gte(start);
+		query.setCreateDate_lte(end);
+		return mesMaterialCkLogService.findList(query);
+	}
+
+	private List<MesMaterialReport> findReports(Date start, Date end) {
+		MesMaterialReport query = new MesMaterialReport();
+		query.setCreateDate_gte(start);
+		query.setCreateDate_lte(end);
+		return mesMaterialReportService.findList(query);
+	}
+
+	private List<MesMaterialCheckRecord> findCheckRecords(Date start, Date end) {
+		MesMaterialCheckRecord query = new MesMaterialCheckRecord();
+		query.setCreateDate_gte(start);
+		query.setCreateDate_lte(end);
+		return mesMaterialCheckRecordService.findList(query);
+	}
+
+	private List<MesMaterialRkRecord> findRkRecords(Date start, Date end) {
+		MesMaterialRkRecord query = new MesMaterialRkRecord();
+		query.setCreateDate_gte(start);
+		query.setCreateDate_lte(end);
+		return mesMaterialRkRecordService.findList(query);
+	}
+
+	private List<MesMaterialLlRecord> findLlRecords(Date start, Date end) {
+		MesMaterialLlRecord query = new MesMaterialLlRecord();
+		query.setCreateDate_gte(start);
+		query.setCreateDate_lte(end);
+		return mesMaterialLlRecordService.findList(query);
+	}
+
+	private List<MesMaterialBlpRecord> findBlpRecords(Date start, Date end) {
+		MesMaterialBlpRecord query = new MesMaterialBlpRecord();
+		query.setCreateDate_gte(start);
+		query.setCreateDate_lte(end);
+		return mesMaterialBlpRecordService.findList(query);
+	}
+
+	private List<MesMaterialThRecord> findThRecords(Date start, Date end) {
+		MesMaterialThRecord query = new MesMaterialThRecord();
+		query.setCreateDate_gte(start);
+		query.setCreateDate_lte(end);
+		return mesMaterialThRecordService.findList(query);
+	}
+
+	private List<MesMaterialBfRecord> findBfRecords(Date start, Date end) {
+		MesMaterialBfRecord query = new MesMaterialBfRecord();
+		query.setCreateDate_gte(start);
+		query.setCreateDate_lte(end);
+		return mesMaterialBfRecordService.findList(query);
+	}
+
+	private List<MesMaterialKcLog> filterPositiveStock(List<MesMaterialKcLog> list) {
+		List<MesMaterialKcLog> result = new ArrayList<>();
+		for (MesMaterialKcLog item : list) {
+			if (item.getNums() != null && item.getNums() > 0) {
+				result.add(item);
+			}
+		}
+		return result;
+	}
+
+	private <T extends DataEntity<T>> List<T> filterByDate(List<T> list, Date start, Date end) {
+		List<T> result = new ArrayList<>();
+		for (T item : list) {
+			Date createDate = item.getCreateDate();
+			if (createDate != null && !createDate.before(start) && !createDate.after(end)) {
+				result.add(item);
+			}
+		}
+		return result;
+	}
+
+	private long sumKc(List<MesMaterialKcLog> list) {
+		long total = 0;
+		for (MesMaterialKcLog item : list) {
+			total += item.getNums() == null ? 0 : item.getNums();
+		}
+		return total;
+	}
+
+	private long sumRk(List<MesMaterialRkLog> list) {
+		long total = 0;
+		for (MesMaterialRkLog item : list) {
+			total += item.getNums() == null ? 0 : item.getNums();
+		}
+		return total;
+	}
+
+	private long sumCk(List<MesMaterialCkLog> list) {
+		long total = 0;
+		for (MesMaterialCkLog item : list) {
+			total += item.getNums() == null ? 0 : item.getNums();
+		}
+		return total;
+	}
+
+	private int countReportState(List<MesMaterialReport> list, String state) {
+		int count = 0;
+		for (MesMaterialReport item : list) {
+			if (state.equals(item.getState())) {
+				count++;
+			}
+		}
+		return count;
+	}
+
+	private int countExpireWarning(List<MesMaterialKcLog> list, Date start, Date end) {
+		int count = 0;
+		for (MesMaterialKcLog item : list) {
+			Date expire = parseDate(item.getExpireDate());
+			if (expire != null && !expire.before(start) && expire.before(end)) {
+				count++;
+			}
+		}
+		return count;
+	}
+
+	private Map<String, Object> buildTrend(Date start, List<MesMaterialRkLog> rkLogs, List<MesMaterialCkLog> ckLogs,
+			List<MesMaterialReport> reports, List<MesMaterialBlpRecord> blpRecords,
+			List<MesMaterialThRecord> thRecords, List<MesMaterialBfRecord> bfRecords) {
+		Map<String, Object> trend = new LinkedHashMap<>();
+		trend.put("inbound", buildDailyQty(start, rkLogs, "rk"));
+		trend.put("outbound", buildDailyQty(start, ckLogs, "ck"));
+		trend.put("report", buildDailyCount(start, reports));
+		trend.put("abnormal", buildDailyAbnormal(start, blpRecords, thRecords, bfRecords));
+		return trend;
+	}
+
+	private <T extends DataEntity<T>> List<Integer> buildDailyCount(Date start, List<T> list) {
+		List<Integer> counts = new ArrayList<>();
+		for (int i = 0; i < 7; i++) {
+			Date s = dayStart(addDays(start, i));
+			Date e = addDays(s, 1);
+			counts.add(filterByDate(list, s, e).size());
+		}
+		return counts;
+	}
+
+	private List<Long> buildDailyQty(Date start, List<?> list, String type) {
+		List<Long> counts = new ArrayList<>();
+		for (int i = 0; i < 7; i++) {
+			Date s = dayStart(addDays(start, i));
+			Date e = addDays(s, 1);
+			long total = 0;
+			for (Object item : list) {
+				if (item instanceof MesMaterialRkLog) {
+					MesMaterialRkLog log = (MesMaterialRkLog)item;
+					if ("rk".equals(type) && log.getCreateDate() != null && !log.getCreateDate().before(s) && log.getCreateDate().before(e)) {
+						total += log.getNums() == null ? 0 : log.getNums();
+					}
+				} else if (item instanceof MesMaterialCkLog) {
+					MesMaterialCkLog log = (MesMaterialCkLog)item;
+					if ("ck".equals(type) && log.getCreateDate() != null && !log.getCreateDate().before(s) && log.getCreateDate().before(e)) {
+						total += log.getNums() == null ? 0 : log.getNums();
+					}
+				}
+			}
+			counts.add(total);
+		}
+		return counts;
+	}
+
+	private List<Integer> buildDailyAbnormal(Date start, List<MesMaterialBlpRecord> blpRecords,
+			List<MesMaterialThRecord> thRecords, List<MesMaterialBfRecord> bfRecords) {
+		List<Integer> counts = new ArrayList<>();
+		for (int i = 0; i < 7; i++) {
+			Date s = dayStart(addDays(start, i));
+			Date e = addDays(s, 1);
+			counts.add(filterByDate(blpRecords, s, e).size() + filterByDate(thRecords, s, e).size() + filterByDate(bfRecords, s, e).size());
+		}
+		return counts;
+	}
+
+	private Map<String, Integer> countReports(List<MesMaterialReport> list) {
+		Map<String, Integer> result = new LinkedHashMap<>();
+		for (MesMaterialReport item : list) {
+			increase(result, "1".equals(item.getState()) ? "已检查" : "未检查");
+		}
+		return result;
+	}
+
+	private Map<String, Integer> countChecks(List<MesMaterialCheckRecord> list) {
+		Map<String, Integer> result = new LinkedHashMap<>();
+		for (MesMaterialCheckRecord item : list) {
+			increase(result, "2".equals(item.getState()) ? "不合格" : "合格");
+		}
+		return result;
+	}
+
+	private Map<String, Integer> countBlp(List<MesMaterialBlpRecord> list) {
+		Map<String, Integer> result = new LinkedHashMap<>();
+		for (MesMaterialBlpRecord item : list) {
+			increase(result, blpStateLabel(item.getState()));
+		}
+		return result;
+	}
+
+	private Map<String, Integer> countRkType(List<MesMaterialRkRecord> list) {
+		Map<String, Integer> result = new LinkedHashMap<>();
+		for (MesMaterialRkRecord item : list) {
+			increase(result, rkTypeLabel(item.getType()));
+		}
+		return result;
+	}
+
+	private Map<String, Integer> countLl(List<MesMaterialLlRecord> list) {
+		Map<String, Integer> result = new LinkedHashMap<>();
+		for (MesMaterialLlRecord item : list) {
+			increase(result, "2".equals(item.getState()) ? "已领料" : "待领料");
+		}
+		return result;
+	}
+
+	private Map<String, Long> countStockByLocation(List<MesMaterialKcLog> list) {
+		Map<String, Long> result = new LinkedHashMap<>();
+		for (MesMaterialKcLog item : list) {
+			increaseLong(result, safeText(item.getKwTitle(), "未记录库位"), item.getNums());
+		}
+		return result;
+	}
+
+	private Map<String, Long> countStockByMaterial(List<MesMaterialKcLog> list) {
+		Map<String, Long> result = new LinkedHashMap<>();
+		for (MesMaterialKcLog item : list) {
+			increaseLong(result, safeText(item.getMaterialTitle(), "未记录物料"), item.getNums());
+		}
+		return result;
+	}
+
+	private List<Map<String, Object>> topStockRows(List<MesMaterialKcLog> list, int limit) {
+		List<MesMaterialKcLog> copy = new ArrayList<>(list);
+		Collections.sort(copy, new Comparator<MesMaterialKcLog>() {
+			@Override
+			public int compare(MesMaterialKcLog o1, MesMaterialKcLog o2) {
+				return Long.valueOf(o2.getNums() == null ? 0 : o2.getNums()).compareTo(o1.getNums() == null ? 0 : o1.getNums());
+			}
+		});
+		List<Map<String, Object>> rows = new ArrayList<>();
+		for (int i = 0; i < Math.min(limit, copy.size()); i++) {
+			MesMaterialKcLog item = copy.get(i);
+			Map<String, Object> row = new LinkedHashMap<>();
+			row.put("material", safeText(item.getMaterialTitle(), "-"));
+			row.put("erpSn", safeText(item.getErpSn(), "-"));
+			row.put("batchSn", safeText(item.getBatchSn(), "-"));
+			row.put("kw", safeText(item.getKwTitle(), "-"));
+			row.put("nums", item.getNums() == null ? 0 : item.getNums());
+			row.put("expireDate", safeText(item.getExpireDate(), "-"));
+			rows.add(row);
+		}
+		return rows;
+	}
+
+	private List<Map<String, Object>> expireRows(List<MesMaterialKcLog> list, Date start, Date end, int limit) {
+		List<MesMaterialKcLog> warning = new ArrayList<>();
+		for (MesMaterialKcLog item : list) {
+			Date expire = parseDate(item.getExpireDate());
+			if (expire != null && !expire.before(start) && expire.before(end)) {
+				warning.add(item);
+			}
+		}
+		Collections.sort(warning, new Comparator<MesMaterialKcLog>() {
+			@Override
+			public int compare(MesMaterialKcLog o1, MesMaterialKcLog o2) {
+				Date d1 = parseDate(o1.getExpireDate());
+				Date d2 = parseDate(o2.getExpireDate());
+				if (d1 == null && d2 == null) {
+					return 0;
+				}
+				if (d1 == null) {
+					return 1;
+				}
+				if (d2 == null) {
+					return -1;
+				}
+				return d1.compareTo(d2);
+			}
+		});
+		List<Map<String, Object>> rows = new ArrayList<>();
+		for (int i = 0; i < Math.min(limit, warning.size()); i++) {
+			MesMaterialKcLog item = warning.get(i);
+			Map<String, Object> row = new LinkedHashMap<>();
+			row.put("material", safeText(item.getMaterialTitle(), "-"));
+			row.put("batchSn", safeText(item.getBatchSn(), "-"));
+			row.put("kw", safeText(item.getKwTitle(), "-"));
+			row.put("nums", item.getNums() == null ? 0 : item.getNums());
+			row.put("expireDate", safeText(item.getExpireDate(), "-"));
+			rows.add(row);
+		}
+		return rows;
+	}
+
+	private List<Map<String, Object>> latestActions(List<MesMaterialReport> reports, List<MesMaterialCheckRecord> checks,
+			List<MesMaterialRkLog> rkLogs, List<MesMaterialCkLog> ckLogs, List<MesMaterialBlpRecord> blpRecords,
+			List<MesMaterialThRecord> thRecords, List<MesMaterialBfRecord> bfRecords, int limit) {
+		List<Map<String, Object>> rows = new ArrayList<>();
+		for (MesMaterialReport item : reports) {
+			rows.add(actionRow("来料报检", item.getTitle(), item.getSn(), reportStateLabel(item.getState()), item.getNums(), item.getCreateDate()));
+		}
+		for (MesMaterialCheckRecord item : checks) {
+			rows.add(actionRow("来料检查", item.getMaterialTitle(), item.getSn(), checkStateLabel(item.getState()), item.getNums(), item.getCreateDate()));
+		}
+		for (MesMaterialRkLog item : rkLogs) {
+			rows.add(actionRow("入库", item.getMaterialTitle(), item.getBatchSn(), safeText(item.getKwTitle(), "-"), item.getNums(), item.getCreateDate()));
+		}
+		for (MesMaterialCkLog item : ckLogs) {
+			rows.add(actionRow("出库", item.getMaterialTitle(), item.getBatchSn(), safeText(item.getRemark(), "-"), item.getNums(), item.getCreateDate()));
+		}
+		for (MesMaterialBlpRecord item : blpRecords) {
+			rows.add(actionRow("不良品", item.getTitle(), "-", blpStateLabel(item.getState()), parseLong(item.getNums()), item.getCreateDate()));
+		}
+		for (MesMaterialThRecord item : thRecords) {
+			rows.add(actionRow("退货", "-", "-", blpStateLabel(item.getState()), null, item.getCreateDate()));
+		}
+		for (MesMaterialBfRecord item : bfRecords) {
+			rows.add(actionRow("报废", "-", "-", blpStateLabel(item.getState()), null, item.getCreateDate()));
+		}
+		Collections.sort(rows, new Comparator<Map<String, Object>>() {
+			@Override
+			public int compare(Map<String, Object> o1, Map<String, Object> o2) {
+				Date d1 = (Date)o1.get("_date");
+				Date d2 = (Date)o2.get("_date");
+				if (d1 == null && d2 == null) {
+					return 0;
+				}
+				if (d1 == null) {
+					return 1;
+				}
+				if (d2 == null) {
+					return -1;
+				}
+				return d2.compareTo(d1);
+			}
+		});
+		List<Map<String, Object>> result = new ArrayList<>();
+		for (int i = 0; i < Math.min(limit, rows.size()); i++) {
+			Map<String, Object> row = rows.get(i);
+			row.remove("_date");
+			result.add(row);
+		}
+		return result;
+	}
+
+	private Map<String, Object> actionRow(String type, String material, String batch, String state, Long nums, Date date) {
+		Map<String, Object> row = new LinkedHashMap<>();
+		row.put("type", type);
+		row.put("material", safeText(material, "-"));
+		row.put("batch", safeText(batch, "-"));
+		row.put("state", safeText(state, "-"));
+		row.put("nums", nums == null ? "-" : nums);
+		row.put("time", date == null ? "" : new SimpleDateFormat("MM-dd HH:mm").format(date));
+		row.put("_date", date);
+		return row;
+	}
+
+	private List<Map<String, Object>> toChartData(Map<String, ? extends Number> source) {
+		List<Map<String, Object>> result = new ArrayList<>();
+		for (Map.Entry<String, ? extends Number> entry : source.entrySet()) {
+			Map<String, Object> item = new LinkedHashMap<>();
+			item.put("name", entry.getKey());
+			item.put("value", entry.getValue());
+			result.add(item);
+		}
+		return result;
+	}
+
+	private <N extends Number> Map<String, N> top(Map<String, N> source, int limit) {
+		List<Map.Entry<String, N>> entries = new ArrayList<>(source.entrySet());
+		Collections.sort(entries, new Comparator<Map.Entry<String, N>>() {
+			@Override
+			public int compare(Map.Entry<String, N> o1, Map.Entry<String, N> o2) {
+				return Long.valueOf(o2.getValue().longValue()).compareTo(o1.getValue().longValue());
+			}
+		});
+		Map<String, N> result = new LinkedHashMap<>();
+		for (int i = 0; i < Math.min(limit, entries.size()); i++) {
+			result.put(entries.get(i).getKey(), entries.get(i).getValue());
+		}
+		return result;
+	}
+
+	private Date dayStart(Date date) {
+		Calendar calendar = Calendar.getInstance();
+		calendar.setTime(date);
+		calendar.set(Calendar.HOUR_OF_DAY, 0);
+		calendar.set(Calendar.MINUTE, 0);
+		calendar.set(Calendar.SECOND, 0);
+		calendar.set(Calendar.MILLISECOND, 0);
+		return calendar.getTime();
+	}
+
+	private Date addDays(Date date, int days) {
+		Calendar calendar = Calendar.getInstance();
+		calendar.setTime(date);
+		calendar.add(Calendar.DAY_OF_MONTH, days);
+		return calendar.getTime();
+	}
+
+	private List<String> buildDayLabels(Date start) {
+		List<String> labels = new ArrayList<>();
+		SimpleDateFormat formatter = new SimpleDateFormat("MM-dd");
+		for (int i = 0; i < 7; i++) {
+			labels.add(formatter.format(addDays(start, i)));
+		}
+		return labels;
+	}
+
+	private Date parseDate(String text) {
+		if (StringUtils.isBlank(text)) {
+			return null;
+		}
+		String value = text.trim();
+		String[] patterns = {"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM-dd"};
+		for (String pattern : patterns) {
+			try {
+				return new SimpleDateFormat(pattern).parse(value);
+			} catch (ParseException ignored) {
+			}
+		}
+		return null;
+	}
+
+	private void increase(Map<String, Integer> map, String key) {
+		map.put(key, map.containsKey(key) ? map.get(key) + 1 : 1);
+	}
+
+	private void increaseLong(Map<String, Long> map, String key, Long value) {
+		long current = map.containsKey(key) ? map.get(key) : 0;
+		map.put(key, current + (value == null ? 0 : value));
+	}
+
+	private Long parseLong(String value) {
+		if (StringUtils.isBlank(value)) {
+			return null;
+		}
+		try {
+			return Long.valueOf(value);
+		} catch (NumberFormatException ex) {
+			return null;
+		}
+	}
+
+	private String reportStateLabel(String value) {
+		return "1".equals(value) ? "已检查" : "未检查";
+	}
+
+	private String checkStateLabel(String value) {
+		return "2".equals(value) ? "不合格" : "合格";
+	}
+
+	private String blpStateLabel(String value) {
+		if ("1".equals(value)) {
+			return "待审核";
+		} else if ("2".equals(value)) {
+			return "通过";
+		} else if ("3".equals(value)) {
+			return "未通过";
+		}
+		return "未知";
+	}
+
+	private String rkTypeLabel(String value) {
+		if ("1".equals(value)) {
+			return "正常入库";
+		} else if ("2".equals(value)) {
+			return "让步入库";
+		} else if ("3".equals(value)) {
+			return "余料入库";
+		}
+		return "未知";
+	}
+
+	private String safeText(String text, String fallback) {
+		return StringUtils.isBlank(text) ? fallback : text;
+	}
+
 	@RequiresPermissions("mes:mesMaterial:view")
 	@RequestMapping(value = "exportData")
 	public void exportData(MesMaterial mesMaterial, HttpServletResponse response) {
 		List<MesMaterial> list = mesMaterialService.findList(mesMaterial);
 		String fileName = "物料表" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
-		try(ExcelExport ee = new ExcelExport("物料表", MesMaterial.class)){
+		try(ExcelExport ee = new ExcelExport("物料表", MesNg.class)){
 			ee.setDataList(list).write(response, fileName);
 		}
 	}
@@ -153,75 +746,4 @@ public class MesMaterialController extends BaseController {
 		model.addAttribute("warehouse", mesMaterial);
 		return "modules/mes/mesMaterialSelect";
 	}
-	/**
-	 * 下载模板
-	 */
-	@RequestMapping(value = "importTemplate")
-	public void importTemplate(HttpServletResponse response) {
-		MesMaterial mesDeviceCate = new MesMaterial();
-		List<MesMaterial> list = ListUtils.newArrayList(mesDeviceCate);
-		String fileName = "物料表.xlsx";
-		try(ExcelExport ee = new ExcelExport("物料表", MesMaterial.class, ExcelField.Type.IMPORT)){
-			ee.setDataList(list).write(response, fileName);
-		}
-	}
-
-	/**
-	 * 导入数据
-	 */
-	@ResponseBody
-	@PostMapping(value = "importData")
-	public String importData(MultipartFile file) {
-		try {
-			String message = importData1(file);
-			return renderResult(Global.TRUE, "posfull:"+message);
-		} catch (Exception ex) {
-			return renderResult(Global.FALSE, "posfull:"+ex.getMessage());
-		}
-	}
-	@Transactional
-	public String importData1(MultipartFile file) {
-		if (file == null){
-			throw new ServiceException(text("请选择导入的数据文件!"));
-		}
-		int successNum = 0; int failureNum = 0;
-		StringBuilder successMsg = new StringBuilder();
-		StringBuilder failureMsg = new StringBuilder();
-		try(ExcelImport ei = new ExcelImport(file, 2, 0)){
-			List<MesMaterial> list = ei.getDataList(MesMaterial.class);
-			for (MesMaterial mesDeviceCheck : list) {
-				try{
-					ValidatorUtils.validateWithException(mesDeviceCheck);
-					this.save(mesDeviceCheck);
-					successNum++;
-					successMsg.append("<br/>" + successNum + "、编号 " + mesDeviceCheck.getId() + " 导入成功");
-				} catch (Exception e) {
-					failureNum++;
-					String msg = "<br/>" + failureNum + "、编号 " + mesDeviceCheck.getId() + " 导入失败:";
-					if (e instanceof ConstraintViolationException){
-						ConstraintViolationException cve = (ConstraintViolationException)e;
-						for (ConstraintViolation<?> violation : cve.getConstraintViolations()) {
-							msg += Global.getText(violation.getMessage()) + " ("+violation.getPropertyPath()+")";
-						}
-					}else{
-						msg += e.getMessage();
-					}
-					failureMsg.append(msg);
-					logger.error(msg, e);
-				}
-			}
-		} catch (Exception e) {
-			logger.error(e.getMessage(), e);
-			failureMsg.append(e.getMessage());
-			return failureMsg.toString();
-		}
-		if (failureNum > 0) {
-			failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
-			throw new ServiceException(failureMsg.toString());
-		}else{
-			successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
-		}
-		return successMsg.toString();
-	}
-
-}
+}

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

@@ -4,6 +4,7 @@ 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.DataEntity;
 import com.jeesite.common.entity.Page;
 import com.jeesite.common.lang.DateUtils;
 import com.jeesite.common.lang.ObjectUtils;
@@ -85,6 +86,12 @@ public class MesProductController extends BaseController {
 	@Autowired
 	private MesRepairService mesRepairService;
 	@Autowired
+	private MesProductCheckService mesProductCheckService;
+	@Autowired
+	private MesProductCheckFirstService mesProductCheckFirstService;
+	@Autowired
+	private MesNgService mesNgService;
+	@Autowired
 	private MesShippRecordService mesShippRecordService;
 	@Autowired
 	private OcrService ocrService;
@@ -307,6 +314,16 @@ public class MesProductController extends BaseController {
 		return "modules/mes/mesScreen4";
 	}
 
+	@RequestMapping(value = "screen2")
+	public String screen3(Model model) {
+		return "modules/mes/mesScreen2";
+	}
+
+	@RequestMapping(value = "screen3")
+	public String screen4(Model model) {
+		return "modules/mes/mesScreen3";
+	}
+
 	/**
 	 * 六安项目 MES 生产管理模块数据大屏入口。
 	 * 前端页面会继续调用 screenData4、screenDataHourly 等已有数据接口渲染图表。
@@ -316,6 +333,382 @@ public class MesProductController extends BaseController {
 		return "modules/mes/mesScreenLiuan";
 	}
 
+	@RequestMapping(value = "qualityScreen")
+	public String qualityScreen(Model model) {
+		return "modules/mes/mesQualityScreen";
+	}
+
+	@RequestMapping(value = "qualityScreenData")
+	@ResponseBody
+	public CommonResp qualityScreenData() {
+		CommonResp<Map<String, Object>> resp = new CommonResp<>();
+		Map<String, Object> data = new LinkedHashMap<>();
+		Date now = new Date();
+		Date todayStart = dayStart(now);
+		Date weekStart = dayStart(addDays(now, -6));
+
+		List<MesProductCheck> productChecks = findProductChecks(weekStart, now);
+		List<MesProductCheckFirst> firstChecks = findFirstChecks(weekStart, now);
+		List<MesNg> ngList = findNgList(weekStart, now);
+		List<MesRepair> repairList = findRepairList(weekStart, now);
+		List<MesScrap> scrapList = findScrapList(weekStart, now);
+
+		List<MesProductCheck> todayProductChecks = filterByDate(productChecks, todayStart, now);
+		List<MesProductCheckFirst> todayFirstChecks = filterByDate(firstChecks, todayStart, now);
+		List<MesNg> todayNgList = filterByDate(ngList, todayStart, now);
+		List<MesRepair> todayRepairList = filterByDate(repairList, todayStart, now);
+		List<MesScrap> todayScrapList = filterByDate(scrapList, todayStart, now);
+
+		Map<String, Object> summary = new LinkedHashMap<>();
+		summary.put("checkCount", todayProductChecks.size());
+		summary.put("firstCheckCount", todayFirstChecks.size());
+		summary.put("ngCount", todayNgList.size());
+		summary.put("repairCount", todayRepairList.size());
+		summary.put("scrapCount", todayScrapList.size());
+		summary.put("pendingRepairCount", countPendingRepair(todayRepairList));
+		data.put("summary", summary);
+
+		data.put("days", buildDayLabels(weekStart));
+		data.put("trend", buildQualityTrend(weekStart, productChecks, ngList, repairList, scrapList));
+		data.put("checkStatus", toChartData(countProductCheckStatus(productChecks)));
+		data.put("checkType", toChartData(countProductCheckType(productChecks)));
+		data.put("firstCheckStatus", toChartData(countFirstCheckStatus(firstChecks)));
+		data.put("ngType", toChartData(countNgType(ngList)));
+		data.put("repairState", toChartData(countRepairState(repairList)));
+		data.put("repairResult", toChartData(countRepairResult(repairList)));
+		data.put("scrapReasonTop", toChartData(top(countByScrapReason(scrapList), 8)));
+		data.put("stationTop", toChartData(top(countByStation(productChecks, ngList, repairList), 8)));
+		data.put("latestIssues", latestIssues(productChecks, ngList, repairList, scrapList, 12));
+
+		resp.setData(data);
+		resp.setResult(Global.TRUE);
+		return resp;
+	}
+
+	private List<MesProductCheck> findProductChecks(Date start, Date end) {
+		MesProductCheck query = new MesProductCheck();
+		query.getSqlMap().getWhere().and("create_date", QueryType.GTE, start);
+		query.getSqlMap().getWhere().and("create_date", QueryType.LTE, end);
+		return mesProductCheckService.findList(query);
+	}
+
+	private List<MesProductCheckFirst> findFirstChecks(Date start, Date end) {
+		MesProductCheckFirst query = new MesProductCheckFirst();
+		query.setCreateDate_gte(start);
+		query.setCreateDate_lte(end);
+		return mesProductCheckFirstService.findList(query);
+	}
+
+	private List<MesNg> findNgList(Date start, Date end) {
+		MesNg query = new MesNg();
+		query.getSqlMap().getWhere().and("create_date", QueryType.GTE, start);
+		query.getSqlMap().getWhere().and("create_date", QueryType.LTE, end);
+		return mesNgService.findList(query);
+	}
+
+	private List<MesRepair> findRepairList(Date start, Date end) {
+		MesRepair query = new MesRepair();
+		query.setCreateDate_gte(start);
+		query.setCreateDate_lte(end);
+		return mesRepairService.findList(query);
+	}
+
+	private List<MesScrap> findScrapList(Date start, Date end) {
+		MesScrap query = new MesScrap();
+		query.getSqlMap().getWhere().and("create_date", QueryType.GTE, start);
+		query.getSqlMap().getWhere().and("create_date", QueryType.LTE, end);
+		return mesScrapService.findList(query);
+	}
+
+	private <T extends DataEntity<T>> List<T> filterByDate(List<T> list, Date start, Date end) {
+		List<T> result = new ArrayList<>();
+		for (T item : list) {
+			Date createDate = item.getCreateDate();
+			if (createDate != null && !createDate.before(start) && !createDate.after(end)) {
+				result.add(item);
+			}
+		}
+		return result;
+	}
+
+	private Date dayStart(Date date) {
+		Calendar calendar = Calendar.getInstance();
+		calendar.setTime(date);
+		calendar.set(Calendar.HOUR_OF_DAY, 0);
+		calendar.set(Calendar.MINUTE, 0);
+		calendar.set(Calendar.SECOND, 0);
+		calendar.set(Calendar.MILLISECOND, 0);
+		return calendar.getTime();
+	}
+
+	private Date addDays(Date date, int days) {
+		Calendar calendar = Calendar.getInstance();
+		calendar.setTime(date);
+		calendar.add(Calendar.DAY_OF_MONTH, days);
+		return calendar.getTime();
+	}
+
+	private List<String> buildDayLabels(Date start) {
+		List<String> labels = new ArrayList<>();
+		SimpleDateFormat formatter = new SimpleDateFormat("MM-dd");
+		for (int i = 0; i < 7; i++) {
+			labels.add(formatter.format(addDays(start, i)));
+		}
+		return labels;
+	}
+
+	private Map<String, Object> buildQualityTrend(Date start, List<MesProductCheck> checks, List<MesNg> ngList,
+			List<MesRepair> repairList, List<MesScrap> scrapList) {
+		Map<String, Object> trend = new LinkedHashMap<>();
+		trend.put("checks", buildDailyCounts(start, checks));
+		trend.put("ng", buildDailyCounts(start, ngList));
+		trend.put("repair", buildDailyCounts(start, repairList));
+		trend.put("scrap", buildDailyCounts(start, scrapList));
+		return trend;
+	}
+
+	private <T extends DataEntity<T>> List<Integer> buildDailyCounts(Date start, List<T> list) {
+		List<Integer> counts = new ArrayList<>();
+		for (int i = 0; i < 7; i++) {
+			Date dayStart = dayStart(addDays(start, i));
+			Date dayEnd = addDays(dayStart, 1);
+			int count = 0;
+			for (T item : list) {
+				Date createDate = item.getCreateDate();
+				if (createDate != null && !createDate.before(dayStart) && createDate.before(dayEnd)) {
+					count++;
+				}
+			}
+			counts.add(count);
+		}
+		return counts;
+	}
+
+	private int countPendingRepair(List<MesRepair> list) {
+		int count = 0;
+		for (MesRepair item : list) {
+			if (!"3".equals(item.getState())) {
+				count++;
+			}
+		}
+		return count;
+	}
+
+	private Map<String, Integer> countProductCheckStatus(List<MesProductCheck> list) {
+		Map<String, Integer> result = new LinkedHashMap<>();
+		for (MesProductCheck item : list) {
+			increase(result, productCheckStatusLabel(item.getState()));
+		}
+		return result;
+	}
+
+	private Map<String, Integer> countProductCheckType(List<MesProductCheck> list) {
+		Map<String, Integer> result = new LinkedHashMap<>();
+		for (MesProductCheck item : list) {
+			increase(result, productCheckTypeLabel(item.getType()));
+		}
+		return result;
+	}
+
+	private Map<String, Integer> countFirstCheckStatus(List<MesProductCheckFirst> list) {
+		Map<String, Integer> result = new LinkedHashMap<>();
+		for (MesProductCheckFirst item : list) {
+			increase(result, "1".equals(item.getState()) ? "已检查" : "未检查");
+		}
+		return result;
+	}
+
+	private Map<String, Integer> countNgType(List<MesNg> list) {
+		Map<String, Integer> result = new LinkedHashMap<>();
+		for (MesNg item : list) {
+			increase(result, "2".equals(item.getType()) ? "离线返修" : "在线返修");
+		}
+		return result;
+	}
+
+	private Map<String, Integer> countRepairState(List<MesRepair> list) {
+		Map<String, Integer> result = new LinkedHashMap<>();
+		for (MesRepair item : list) {
+			increase(result, repairStateLabel(item.getState()));
+		}
+		return result;
+	}
+
+	private Map<String, Integer> countRepairResult(List<MesRepair> list) {
+		Map<String, Integer> result = new LinkedHashMap<>();
+		for (MesRepair item : list) {
+			increase(result, repairResultLabel(item.getResult()));
+		}
+		return result;
+	}
+
+	private Map<String, Integer> countByScrapReason(List<MesScrap> list) {
+		Map<String, Integer> result = new LinkedHashMap<>();
+		for (MesScrap item : list) {
+			increase(result, safeText(item.getReason(), "未填写原因"));
+		}
+		return result;
+	}
+
+	private Map<String, Integer> countByStation(List<MesProductCheck> checks, List<MesNg> ngList, List<MesRepair> repairList) {
+		Map<String, Integer> result = new LinkedHashMap<>();
+		for (MesProductCheck item : checks) {
+			increase(result, safeText(item.getOprno(), "未记录工位"));
+		}
+		for (MesNg item : ngList) {
+			increase(result, safeText(item.getOprno(), "未记录工位"));
+		}
+		for (MesRepair item : repairList) {
+			increase(result, safeText(item.getOprno(), "未记录工位"));
+		}
+		return result;
+	}
+
+	private List<Map<String, Object>> latestIssues(List<MesProductCheck> checks, List<MesNg> ngList,
+			List<MesRepair> repairList, List<MesScrap> scrapList, int limit) {
+		List<Map<String, Object>> rows = new ArrayList<>();
+		for (MesProductCheck item : checks) {
+			if (!"1".equals(item.getState())) {
+				rows.add(issueRow("过程检查", item.getSn(), item.getOprno(), safeText(item.getCheckTitle(), item.getItems()),
+						productCheckStatusLabel(item.getState()), item.getCreateDate()));
+			}
+		}
+		for (MesNg item : ngList) {
+			rows.add(issueRow("NG记录", item.getSn(), item.getOprno(), safeText(item.getItems(), item.getRemark()),
+					"2".equals(item.getType()) ? "离线返修" : "在线返修", item.getCreateDate()));
+		}
+		for (MesRepair item : repairList) {
+			rows.add(issueRow("返修记录", item.getSn(), item.getOprno(), safeText(item.getItemTitle(), item.getRemark()),
+					repairStateLabel(item.getState()), item.getCreateDate()));
+		}
+		for (MesScrap item : scrapList) {
+			rows.add(issueRow("报废记录", item.getSn(), item.getProductCate(), safeText(item.getReason(), "未填写原因"),
+					"报废", item.getCreateDate()));
+		}
+		Collections.sort(rows, new Comparator<Map<String, Object>>() {
+			@Override
+			public int compare(Map<String, Object> o1, Map<String, Object> o2) {
+				Date d1 = (Date)o1.get("_date");
+				Date d2 = (Date)o2.get("_date");
+				if (d1 == null && d2 == null) {
+					return 0;
+				}
+				if (d1 == null) {
+					return 1;
+				}
+				if (d2 == null) {
+					return -1;
+				}
+				return d2.compareTo(d1);
+			}
+		});
+		List<Map<String, Object>> result = new ArrayList<>();
+		int size = Math.min(limit, rows.size());
+		for (int i = 0; i < size; i++) {
+			Map<String, Object> row = rows.get(i);
+			row.remove("_date");
+			result.add(row);
+		}
+		return result;
+	}
+
+	private Map<String, Object> issueRow(String type, String sn, String station, String content, String state, Date date) {
+		Map<String, Object> row = new LinkedHashMap<>();
+		row.put("type", type);
+		row.put("sn", safeText(sn, "-"));
+		row.put("station", safeText(station, "-"));
+		row.put("content", safeText(content, "-"));
+		row.put("state", safeText(state, "-"));
+		row.put("time", date == null ? "" : new SimpleDateFormat("MM-dd HH:mm").format(date));
+		row.put("_date", date);
+		return row;
+	}
+
+	private List<Map<String, Object>> toChartData(Map<String, Integer> source) {
+		List<Map<String, Object>> result = new ArrayList<>();
+		for (Map.Entry<String, Integer> entry : source.entrySet()) {
+			Map<String, Object> item = new LinkedHashMap<>();
+			item.put("name", entry.getKey());
+			item.put("value", entry.getValue());
+			result.add(item);
+		}
+		return result;
+	}
+
+	private Map<String, Integer> top(Map<String, Integer> source, int limit) {
+		List<Map.Entry<String, Integer>> entries = new ArrayList<>(source.entrySet());
+		Collections.sort(entries, new Comparator<Map.Entry<String, Integer>>() {
+			@Override
+			public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
+				return o2.getValue().compareTo(o1.getValue());
+			}
+		});
+		Map<String, Integer> result = new LinkedHashMap<>();
+		int size = Math.min(limit, entries.size());
+		for (int i = 0; i < size; i++) {
+			result.put(entries.get(i).getKey(), entries.get(i).getValue());
+		}
+		return result;
+	}
+
+	private void increase(Map<String, Integer> map, String key) {
+		map.put(key, map.containsKey(key) ? map.get(key) + 1 : 1);
+	}
+
+	private String productCheckTypeLabel(String value) {
+		if ("0".equals(value)) {
+			return "随机检查";
+		} else if ("1".equals(value)) {
+			return "首件检查";
+		} else if ("2".equals(value)) {
+			return "下线目视";
+		} else if ("3".equals(value)) {
+			return "手动抽查";
+		} else if ("4".equals(value)) {
+			return "追溯检查";
+		}
+		return "其他检查";
+	}
+
+	private String productCheckStatusLabel(String value) {
+		if ("0".equals(value)) {
+			return "待检测";
+		} else if ("1".equals(value)) {
+			return "合格";
+		} else if ("2".equals(value)) {
+			return "返修";
+		} else if ("3".equals(value)) {
+			return "作废";
+		} else if ("4".equals(value)) {
+			return "返工";
+		}
+		return "未知";
+	}
+
+	private String repairStateLabel(String value) {
+		if ("1".equals(value)) {
+			return "待处理";
+		} else if ("2".equals(value)) {
+			return "返修中";
+		} else if ("3".equals(value)) {
+			return "已检查";
+		}
+		return "未知";
+	}
+
+	private String repairResultLabel(String value) {
+		if ("1".equals(value)) {
+			return "OK";
+		} else if ("2".equals(value)) {
+			return "NG";
+		}
+		return "未确认";
+	}
+
+	private String safeText(String text, String fallback) {
+		return StringUtils.isBlank(text) ? fallback : text;
+	}
+
 	@RequestMapping(value = "alarmData")
 	@ResponseBody
 	public CommonResp alarmData() {

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

@@ -622,10 +622,21 @@ shiro:
     ${adminPath}/mes/mesProductRecord/qmresult = anon
     ${adminPath}/mes/mesProductRecord/device = anon
     ${adminPath}/mes/mesProduct/screen = anon
+    ${adminPath}/mes/mesProduct/screen2 = anon
+    ${adminPath}/mes/mesProduct/screen3 = anon
+    ${adminPath}/mes/mesProduct/screen4 = anon
+    ${adminPath}/mes/mesProduct/screen5 = anon
     ${adminPath}/mes/mesProduct/screenLiuan = anon
+    ${adminPath}/mes/mesProduct/qualityScreen = anon
+    ${adminPath}/mes/mesMaterial/materialScreen = anon
+    ${adminPath}/mes/mesDevice/screen5Data = anon
+    ${adminPath}/mes/mesDevice/oeeScreen = anon
+    ${adminPath}/mes/mesDevice/oeeScreenData = anon
     ${adminPath}/mes/mesProduct/screenData = anon
     ${adminPath}/mes/mesProduct/screenData4 = anon
     ${adminPath}/mes/mesProduct/screenDataHourly = anon
+    ${adminPath}/mes/mesProduct/qualityScreenData = anon
+    ${adminPath}/mes/mesMaterial/materialScreenData = anon
     ${adminPath}/mes/mesProduct/alarmData = anon
     ${adminPath}/mes/mesProductCcd/testDate = anon
     ${adminPath}/mes/mesProductCcd/add = anon
@@ -927,7 +938,7 @@ file:
   enabled: true
 
   #  # 文件上传根路径,设置路径中不允许包含“userfiles”,在指定目录中系统会自动创建userfiles目录,如果不设置默认为contextPath路径
-  baseDir: E:/mescloud
+  baseDir: D:/mescloud
   #  baseDir: /www/wwwroot/mes_cloud_dy
   #
   #  # 上传文件的相对路径(支持:yyyy, MM, dd, HH, mm, ss, E)

+ 15 - 2
src/main/resources/views/modules/mes/mesDeviceForm.html

@@ -1,4 +1,4 @@
-<% layout('/layouts/default.html', {title: '设备管理', libs: ['validate']}){ %>
+<% layout('/layouts/default.html', {title: '设备管理', libs: ['validate','fileupload']}){ %>
 <div class="main-content">
 	<div class="box box-main">
 		<div class="box-header with-border">
@@ -164,6 +164,19 @@
 							</div>
 						</div>
 					</div>
+					<div class="col-xs-12">
+						<div class="form-group">
+							<label class="control-label col-sm-2" title="">
+								<span class="required hide">*</span> ${text('设备图片')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-10">
+								<#form:fileupload id="deviceImageUpload" returnPath="true"
+									filePathInputId="imagePath" fileNameInputId="imageName"
+									uploadType="image" readonly="false" preview="true" maxUploadNum="1" isMini="false"/>
+								<#form:input path="imagePath" maxlength="1000" class="form-control hide"/>
+								<#form:input name="imageName" value="" class="form-control hide"/>
+							</div>
+						</div>
+					</div>
 				</div>
 			</div>
 			<div class="box-footer">
@@ -205,4 +218,4 @@ function treeselectCallback(id, act, index, layero){
 		});
 	}
 }
-</script>
+</script>

+ 655 - 0
src/main/resources/views/modules/mes/mesDeviceOeeScreen.html

@@ -0,0 +1,655 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>设备综合效率管理</title>
+	<style>
+		* {
+			box-sizing: border-box;
+		}
+		html,
+		body {
+			width: 100%;
+			height: 100%;
+			margin: 0;
+			overflow: hidden;
+			background: #061526;
+			color: #d7f6ff;
+			font-family: "Microsoft YaHei", Arial, sans-serif;
+		}
+		.oee-screen {
+			position: relative;
+			width: 100vw;
+			height: 100vh;
+			min-width: 1280px;
+			min-height: 720px;
+			padding: clamp(12px, 1vw, 20px);
+			background:
+				radial-gradient(circle at 52% 44%, rgba(13, 102, 154, .24), transparent 32%),
+				linear-gradient(180deg, #071426 0%, #081d31 48%, #061323 100%);
+		}
+		.oee-screen:before {
+			content: "";
+			position: absolute;
+			inset: 0;
+			pointer-events: none;
+			background-image:
+				linear-gradient(rgba(72, 191, 255, .05) 1px, transparent 1px),
+				linear-gradient(90deg, rgba(72, 191, 255, .05) 1px, transparent 1px);
+			background-size: 42px 42px;
+			mask-image: linear-gradient(to bottom, rgba(0, 0, 0, .8), rgba(0, 0, 0, .2));
+		}
+		.oee-shell {
+			position: relative;
+			z-index: 1;
+			display: grid;
+			grid-template-rows: 76px 1fr;
+			gap: 14px;
+			width: 100%;
+			height: 100%;
+		}
+		.oee-top {
+			position: relative;
+			display: grid;
+			grid-template-columns: 1fr minmax(420px, 720px) 1fr;
+			align-items: center;
+			min-height: 0;
+		}
+		.oee-title {
+			position: relative;
+			text-align: center;
+			font-size: clamp(28px, 2.3vw, 44px);
+			font-weight: 700;
+			letter-spacing: 0;
+			color: #ecfbff;
+			text-shadow: 0 0 18px rgba(65, 220, 255, .85);
+		}
+		.oee-title:before,
+		.oee-title:after {
+			content: "";
+			position: absolute;
+			top: 50%;
+			width: 190px;
+			height: 1px;
+			background: linear-gradient(90deg, transparent, rgba(74, 214, 255, .95));
+		}
+		.oee-title:before {
+			right: calc(100% + 18px);
+		}
+		.oee-title:after {
+			left: calc(100% + 18px);
+			transform: rotate(180deg);
+		}
+		.oee-time {
+			justify-self: end;
+			font-size: 16px;
+			color: #8bd9ff;
+			white-space: nowrap;
+		}
+		.oee-main {
+			display: grid;
+			grid-template-columns: minmax(360px, 24vw) 1fr;
+			gap: 14px;
+			min-height: 0;
+		}
+		.oee-left {
+			display: grid;
+			grid-template-rows: 96px minmax(270px, 1fr) minmax(260px, 1fr);
+			gap: 14px;
+			min-height: 0;
+		}
+		.oee-panel,
+		.oee-card {
+			position: relative;
+			border: 1px solid rgba(65, 202, 255, .48);
+			background: linear-gradient(180deg, rgba(8, 36, 64, .9), rgba(5, 23, 42, .74));
+			box-shadow: inset 0 0 26px rgba(43, 169, 231, .14), 0 0 18px rgba(0, 162, 255, .09);
+			overflow: hidden;
+		}
+		.oee-panel:before,
+		.oee-card:before {
+			content: "";
+			position: absolute;
+			inset: 0;
+			border: 1px solid rgba(141, 236, 255, .08);
+			pointer-events: none;
+		}
+		.oee-kpis {
+			display: grid;
+			grid-template-columns: repeat(3, minmax(0, 1fr));
+			gap: 8px;
+			padding: 8px;
+		}
+		.oee-kpi {
+			position: relative;
+			display: flex;
+			flex-direction: column;
+			justify-content: center;
+			padding: 10px 12px;
+			border: 1px solid rgba(74, 210, 255, .22);
+			background: linear-gradient(135deg, rgba(18, 75, 122, .78), rgba(5, 25, 44, .6));
+			min-width: 0;
+		}
+		.oee-kpi-label {
+			color: #94cde4;
+			font-size: 13px;
+			line-height: 18px;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+		}
+		.oee-kpi-value {
+			margin-top: 8px;
+			font-family: Impact, "Arial Narrow", sans-serif;
+			font-size: clamp(24px, 1.85vw, 34px);
+			line-height: 1;
+			color: #36f2ff;
+			text-shadow: 0 0 15px rgba(54, 242, 255, .52);
+		}
+		.oee-panel-title {
+			height: 42px;
+			display: flex;
+			align-items: center;
+			padding: 0 16px;
+			border-bottom: 1px solid rgba(72, 196, 255, .25);
+			color: #eefcff;
+			font-size: 17px;
+			font-weight: 700;
+		}
+		.oee-panel-title:before {
+			content: "";
+			width: 4px;
+			height: 18px;
+			margin-right: 9px;
+			background: #36e2ff;
+			box-shadow: 0 0 12px #36e2ff;
+		}
+		.oee-status-body {
+			display: grid;
+			grid-template-columns: 58% 42%;
+			height: calc(100% - 42px);
+			min-height: 0;
+			padding: 10px 12px 12px;
+		}
+		#statusChart {
+			width: 100%;
+			height: 100%;
+			min-height: 190px;
+		}
+		.oee-legend {
+			align-self: center;
+			display: grid;
+			gap: 10px;
+			min-width: 0;
+		}
+		.oee-legend-item {
+			display: grid;
+			grid-template-columns: 10px 1fr auto;
+			align-items: center;
+			gap: 8px;
+			font-size: 13px;
+			color: #a9d6e6;
+			min-width: 0;
+		}
+		.oee-dot {
+			width: 10px;
+			height: 10px;
+			border-radius: 50%;
+		}
+		.oee-rank {
+			height: calc(100% - 42px);
+			padding: 12px;
+			overflow: hidden;
+		}
+		.oee-rank-head,
+		.oee-rank-row {
+			display: grid;
+			grid-template-columns: 38px 1fr 70px;
+			align-items: center;
+			gap: 10px;
+		}
+		.oee-rank-head {
+			height: 28px;
+			color: #73b9d3;
+			font-size: 13px;
+			border-bottom: 1px solid rgba(90, 196, 255, .18);
+		}
+		.oee-rank-list {
+			height: calc(100% - 30px);
+			overflow-y: auto;
+			scrollbar-width: thin;
+			scrollbar-color: rgba(54, 226, 255, .58) rgba(10, 42, 66, .42);
+		}
+		.oee-rank-list::-webkit-scrollbar,
+		.oee-device-grid::-webkit-scrollbar {
+			width: 6px;
+		}
+		.oee-rank-list::-webkit-scrollbar-track,
+		.oee-device-grid::-webkit-scrollbar-track {
+			background: rgba(10, 42, 66, .42);
+		}
+		.oee-rank-list::-webkit-scrollbar-thumb,
+		.oee-device-grid::-webkit-scrollbar-thumb {
+			background: rgba(54, 226, 255, .58);
+			border-radius: 6px;
+		}
+		.oee-rank-row {
+			height: 34px;
+			color: #dff9ff;
+			font-size: 14px;
+			border-bottom: 1px solid rgba(87, 164, 210, .1);
+		}
+		.oee-rank-no {
+			width: 24px;
+			height: 22px;
+			display: inline-flex;
+			align-items: center;
+			justify-content: center;
+			color: #0b2038;
+			background: #36e2ff;
+			font-weight: 700;
+		}
+		.oee-rank-name {
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+		}
+		.oee-right {
+			display: grid;
+			grid-template-rows: 54px 1fr;
+			gap: 12px;
+			min-width: 0;
+			min-height: 0;
+		}
+		.oee-toolbar {
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+			padding: 0 18px;
+		}
+		.oee-toolbar-title {
+			font-size: 18px;
+			font-weight: 700;
+			color: #effdff;
+		}
+		.oee-note {
+			max-width: 760px;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+			color: #78bdd6;
+			font-size: 13px;
+		}
+		.oee-device-grid {
+			display: grid;
+			grid-template-columns: repeat(auto-fit, minmax(245px, 1fr));
+			grid-auto-rows: minmax(238px, 1fr);
+			gap: 12px;
+			min-height: 0;
+			overflow-y: auto;
+			padding-right: 6px;
+			scrollbar-width: thin;
+			scrollbar-color: rgba(54, 226, 255, .58) rgba(10, 42, 66, .42);
+		}
+		.oee-card {
+			display: grid;
+			grid-template-rows: 34px minmax(88px, 1fr) 74px;
+			padding: 10px;
+			min-width: 0;
+			min-height: 0;
+		}
+		.oee-card-head {
+			display: grid;
+			grid-template-columns: 1fr auto;
+			align-items: center;
+			gap: 8px;
+			min-width: 0;
+		}
+		.oee-device-name {
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+			color: #edfaff;
+			font-size: 16px;
+			font-weight: 700;
+		}
+		.oee-status-tag {
+			min-width: 48px;
+			height: 24px;
+			display: inline-flex;
+			align-items: center;
+			justify-content: center;
+			padding: 0 8px;
+			border: 1px solid rgba(255, 255, 255, .16);
+			font-size: 13px;
+			white-space: nowrap;
+		}
+		.oee-status-running {
+			color: #35f2c2;
+			background: rgba(53, 242, 194, .1);
+			border-color: rgba(53, 242, 194, .45);
+		}
+		.oee-status-fault {
+			color: #ff7c88;
+			background: rgba(255, 95, 109, .12);
+			border-color: rgba(255, 95, 109, .5);
+		}
+		.oee-status-maintenance {
+			color: #ffc857;
+			background: rgba(255, 200, 87, .12);
+			border-color: rgba(255, 200, 87, .45);
+		}
+		.oee-status-alarm {
+			color: #ffad6c;
+			background: rgba(255, 138, 61, .12);
+			border-color: rgba(255, 138, 61, .45);
+		}
+		.oee-status-offline,
+		.oee-status-unknown {
+			color: #a8b6ce;
+			background: rgba(125, 141, 168, .12);
+			border-color: rgba(125, 141, 168, .45);
+		}
+		.oee-device-visual {
+			display: grid;
+			grid-template-columns: minmax(0, 1fr) 82px;
+			align-items: center;
+			gap: 8px;
+			min-height: 0;
+		}
+		.oee-device-visual img {
+			width: 100%;
+			height: clamp(88px, 12vh, 150px);
+			object-fit: contain;
+			filter: drop-shadow(0 0 18px rgba(61, 219, 255, .32));
+		}
+		.oee-mini-gauge {
+			width: 78px;
+			height: 78px;
+			border-radius: 50%;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			background: conic-gradient(#35f2c2 var(--rate), rgba(55, 100, 128, .34) 0);
+			position: relative;
+		}
+		.oee-mini-gauge:after {
+			content: "";
+			position: absolute;
+			inset: 8px;
+			border-radius: 50%;
+			background: #082036;
+		}
+		.oee-mini-gauge span {
+			position: relative;
+			z-index: 1;
+			color: #eaffff;
+			font-family: Impact, "Arial Narrow", sans-serif;
+			font-size: 19px;
+		}
+		.oee-card-foot {
+			display: grid;
+			grid-template-columns: 1fr 1fr;
+			gap: 8px;
+			padding-top: 8px;
+			border-top: 1px solid rgba(79, 184, 238, .18);
+			min-width: 0;
+		}
+		.oee-meta {
+			min-width: 0;
+		}
+		.oee-meta-label {
+			color: #7fb9cf;
+			font-size: 12px;
+			line-height: 18px;
+		}
+		.oee-meta-value {
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+			color: #e3f8ff;
+			font-size: 14px;
+			line-height: 22px;
+		}
+		.oee-empty {
+			height: 100%;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			color: #7fb9cf;
+			border: 1px dashed rgba(80, 187, 242, .32);
+			background: rgba(4, 22, 39, .38);
+			font-size: 18px;
+		}
+		@media (max-aspect-ratio: 16/10) {
+			.oee-device-grid {
+				grid-auto-rows: minmax(210px, 1fr);
+			}
+			.oee-card {
+				grid-template-rows: 34px minmax(76px, 1fr) 70px;
+			}
+		}
+	</style>
+</head>
+<body>
+<div class="oee-screen">
+	<div class="oee-shell">
+		<div class="oee-top">
+			<div></div>
+			<div class="oee-title">设备综合效率管理</div>
+			<div class="oee-time"><span id="statDate">--</span> <span id="statTime">--</span></div>
+		</div>
+		<div class="oee-main">
+			<div class="oee-left">
+				<div class="oee-panel oee-kpis">
+					<div class="oee-kpi">
+						<div class="oee-kpi-label">设备总数</div>
+						<div class="oee-kpi-value" id="deviceTotal">0</div>
+					</div>
+					<div class="oee-kpi">
+						<div class="oee-kpi-label">运行设备</div>
+						<div class="oee-kpi-value" id="runningCount">0</div>
+					</div>
+					<div class="oee-kpi">
+						<div class="oee-kpi-label">平均开动率</div>
+						<div class="oee-kpi-value"><span id="avgOpenRate">0</span>%</div>
+					</div>
+				</div>
+				<div class="oee-panel">
+					<div class="oee-panel-title">设备状态总览</div>
+					<div class="oee-status-body">
+						<div id="statusChart"></div>
+						<div class="oee-legend" id="statusLegend"></div>
+					</div>
+				</div>
+				<div class="oee-panel">
+					<div class="oee-panel-title">今日开动率排行</div>
+					<div class="oee-rank">
+						<div class="oee-rank-head">
+							<div>排名</div>
+							<div>设备名称</div>
+							<div>开动率</div>
+						</div>
+						<div class="oee-rank-list" id="rankList"></div>
+					</div>
+				</div>
+			</div>
+			<div class="oee-right">
+				<div class="oee-panel oee-toolbar">
+					<div class="oee-toolbar-title">设备运行监控</div>
+					<div class="oee-note" id="dataNote"></div>
+				</div>
+				<div class="oee-device-grid" id="deviceGrid"></div>
+			</div>
+		</div>
+	</div>
+</div>
+<script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
+<script src="${ctxStatic}/leaflet/js/echarts.min.js"></script>
+<script>
+	var statusChart = echarts.init(document.getElementById('statusChart'));
+	var autoScrollTimers = {};
+	var autoScrollPaused = {};
+
+	function safeNumber(value) {
+		var number = Number(value);
+		return isNaN(number) ? 0 : number;
+	}
+
+	function escapeHtml(value) {
+		return String(value == null ? '' : value)
+			.replace(/&/g, '&amp;')
+			.replace(/</g, '&lt;')
+			.replace(/>/g, '&gt;')
+			.replace(/"/g, '&quot;')
+			.replace(/'/g, '&#39;');
+	}
+
+	function renderStatusChart(statusList) {
+		var chartData = (statusList || []).map(function(item) {
+			return {
+				name: item.name,
+				value: safeNumber(item.value),
+				itemStyle: { color: item.color }
+			};
+		});
+		statusChart.setOption({
+			backgroundColor: 'transparent',
+			tooltip: {
+				trigger: 'item',
+				formatter: '{b}: {c}'
+			},
+			series: [{
+				type: 'pie',
+				radius: ['58%', '76%'],
+				center: ['50%', '50%'],
+				avoidLabelOverlap: true,
+				label: {
+					show: true,
+					position: 'center',
+					formatter: '状态\\n总览',
+					color: '#eafdff',
+					fontSize: 18,
+					lineHeight: 26
+				},
+				labelLine: { show: false },
+				data: chartData.length ? chartData : [{ name: '暂无数据', value: 1, itemStyle: { color: '#31506f' } }]
+			}]
+		});
+	}
+
+	function renderLegend(statusList) {
+		var html = '';
+		(statusList || []).forEach(function(item) {
+			html += '<div class="oee-legend-item">'
+				+ '<span class="oee-dot" style="background:' + escapeHtml(item.color) + '"></span>'
+				+ '<span>' + escapeHtml(item.name) + '</span>'
+				+ '<strong>' + safeNumber(item.value) + '</strong>'
+				+ '</div>';
+		});
+		$('#statusLegend').html(html || '<div class="oee-empty">暂无状态数据</div>');
+	}
+
+	function renderRanking(list) {
+		var html = '';
+		(list || []).forEach(function(item, index) {
+			html += '<div class="oee-rank-row">'
+				+ '<div><span class="oee-rank-no">' + (index + 1) + '</span></div>'
+				+ '<div class="oee-rank-name" title="' + escapeHtml(item.sn) + '">' + escapeHtml(item.name) + '</div>'
+				+ '<div>' + safeNumber(item.openRate).toFixed(1) + '%</div>'
+				+ '</div>';
+		});
+		$('#rankList').html(html || '<div class="oee-empty">暂无排行数据</div>');
+		startAutoScroll('rankList', 90);
+	}
+
+	function renderDevices(list) {
+		var html = '';
+		(list || []).forEach(function(item) {
+			var rate = safeNumber(item.openRate);
+			var statusClass = 'oee-status-' + escapeHtml(item.statusCode || 'unknown');
+			var imageUrl = item.imagePath || ('${ctxStatic}/screen/imgs/small' + safeNumber(item.imageIndex || 1) + '.png');
+			html += '<div class="oee-card">'
+				+ '<div class="oee-card-head">'
+				+ '<div class="oee-device-name" title="' + escapeHtml(item.name) + '">' + escapeHtml(item.name) + '</div>'
+				+ '<div class="oee-status-tag ' + statusClass + '">' + escapeHtml(item.statusName) + '</div>'
+				+ '</div>'
+				+ '<div class="oee-device-visual">'
+				+ '<img src="' + escapeHtml(imageUrl) + '" alt="">'
+				+ '<div class="oee-mini-gauge" style="--rate:' + Math.max(0, Math.min(100, rate)) + '%"><span>' + rate.toFixed(1) + '%</span></div>'
+				+ '</div>'
+				+ '<div class="oee-card-foot">'
+				+ '<div class="oee-meta"><div class="oee-meta-label">设备编号</div><div class="oee-meta-value">' + escapeHtml(item.sn) + '</div></div>'
+				+ '<div class="oee-meta"><div class="oee-meta-label">产线</div><div class="oee-meta-value">' + escapeHtml(item.lineSn) + '</div></div>'
+				+ '<div class="oee-meta"><div class="oee-meta-label">当前状态</div><div class="oee-meta-value">' + escapeHtml(item.stateName) + '</div></div>'
+				+ '<div class="oee-meta"><div class="oee-meta-label">最近上报</div><div class="oee-meta-value">' + escapeHtml(item.lastReportTime) + '</div></div>'
+				+ '</div>'
+				+ '</div>';
+		});
+		$('#deviceGrid').html(html || '<div class="oee-empty">暂无设备数据</div>');
+		startAutoScroll('deviceGrid', 70);
+	}
+
+	function startAutoScroll(id, speed) {
+		var box = document.getElementById(id);
+		if (autoScrollTimers[id]) {
+			clearInterval(autoScrollTimers[id]);
+			autoScrollTimers[id] = null;
+		}
+		if (!box || box.scrollHeight <= box.clientHeight) {
+			if (box) {
+				box.scrollTop = 0;
+			}
+			return;
+		}
+		autoScrollTimers[id] = setInterval(function() {
+			if (autoScrollPaused[id]) {
+				return;
+			}
+			if (box.scrollTop + box.clientHeight >= box.scrollHeight - 2) {
+				box.scrollTop = 0;
+			} else {
+				box.scrollTop += 1;
+			}
+		}, speed);
+	}
+
+	function loadData() {
+		$.post('${ctx}/mes/mesDevice/oeeScreenData', function(ret) {
+			if (!ret || ret.result !== 'true' || !ret.data) {
+				$('#deviceGrid').html('<div class="oee-empty">数据加载失败</div>');
+				return;
+			}
+			var data = ret.data;
+			$('#deviceTotal').text(safeNumber(data.deviceTotal));
+			$('#runningCount').text(safeNumber(data.runningCount));
+			$('#avgOpenRate').text(safeNumber(data.avgOpenRate).toFixed(1));
+			$('#statDate').text(data.statDate || '--');
+			$('#statTime').text(data.statTime || '--');
+			$('#dataNote').text(data.dataNote || '');
+			renderStatusChart(data.statusList);
+			renderLegend(data.statusList);
+			renderRanking(data.ranking);
+			renderDevices(data.devices);
+		});
+	}
+
+	$(function() {
+		loadData();
+		setInterval(loadData, 60000);
+		$('#rankList, #deviceGrid').on('mouseenter', function() {
+			autoScrollPaused[this.id] = true;
+		});
+		$('#rankList, #deviceGrid').on('mouseleave', function() {
+			autoScrollPaused[this.id] = false;
+		});
+		$(window).on('resize', function() {
+			statusChart.resize();
+			startAutoScroll('rankList', 90);
+			startAutoScroll('deviceGrid', 70);
+		});
+	});
+</script>
+</body>
+</html>

+ 506 - 0
src/main/resources/views/modules/mes/mesMaterialScreen.html

@@ -0,0 +1,506 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>物料管理看板</title>
+	<style>
+		* { box-sizing: border-box; }
+		html, body {
+			width: 100%;
+			height: 100%;
+			margin: 0;
+			overflow: hidden;
+			background: #061521;
+			color: #ddfbff;
+			font-family: "Microsoft YaHei", Arial, sans-serif;
+		}
+		.screen {
+			position: relative;
+			width: 100vw;
+			height: 100vh;
+			padding: clamp(12px, 1vw, 20px);
+			background:
+				linear-gradient(130deg, rgba(11, 107, 114, .2), transparent 38%),
+				linear-gradient(235deg, rgba(48, 96, 147, .18), transparent 42%),
+				linear-gradient(180deg, #071623 0%, #092235 52%, #06121e 100%);
+		}
+		.screen:before {
+			content: "";
+			position: absolute;
+			inset: 0;
+			pointer-events: none;
+			background-image:
+				linear-gradient(rgba(94, 222, 255, .045) 1px, transparent 1px),
+				linear-gradient(90deg, rgba(94, 222, 255, .045) 1px, transparent 1px);
+			background-size: 44px 44px;
+		}
+		.shell {
+			position: relative;
+			z-index: 1;
+			display: grid;
+			grid-template-rows: 72px 104px 1fr;
+			gap: 14px;
+			width: 100%;
+			height: 100%;
+			min-width: 1200px;
+			min-height: 680px;
+		}
+		.header {
+			display: grid;
+			grid-template-columns: 1fr minmax(360px, 640px) 1fr;
+			align-items: center;
+		}
+		.title {
+			position: relative;
+			text-align: center;
+			font-size: clamp(30px, 2.25vw, 44px);
+			font-weight: 700;
+			color: #f1ffff;
+			text-shadow: 0 0 18px rgba(61, 226, 255, .85);
+		}
+		.title:before,
+		.title:after {
+			content: "";
+			position: absolute;
+			top: 50%;
+			width: 170px;
+			height: 1px;
+			background: linear-gradient(90deg, transparent, rgba(83, 221, 255, .95));
+		}
+		.title:before { right: calc(100% + 18px); }
+		.title:after { left: calc(100% + 18px); transform: rotate(180deg); }
+		.time {
+			justify-self: end;
+			color: #8edbef;
+			font-size: 16px;
+			white-space: nowrap;
+		}
+		.kpis {
+			display: grid;
+			grid-template-columns: repeat(8, minmax(0, 1fr));
+			gap: 12px;
+		}
+		.kpi,
+		.panel {
+			position: relative;
+			border: 1px solid rgba(72, 206, 255, .42);
+			background: linear-gradient(180deg, rgba(9, 43, 68, .9), rgba(6, 24, 39, .78));
+			box-shadow: inset 0 0 24px rgba(52, 194, 228, .13), 0 0 20px rgba(0, 148, 210, .08);
+			overflow: hidden;
+		}
+		.kpi:before,
+		.panel:before {
+			content: "";
+			position: absolute;
+			inset: 0;
+			border: 1px solid rgba(158, 238, 255, .08);
+			pointer-events: none;
+		}
+		.kpi {
+			display: flex;
+			flex-direction: column;
+			justify-content: center;
+			padding: 12px 14px;
+			min-width: 0;
+		}
+		.kpi-label {
+			color: #a1cfdb;
+			font-size: 13px;
+			line-height: 18px;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+		}
+		.kpi-value {
+			margin-top: 8px;
+			font-family: Impact, "Arial Narrow", sans-serif;
+			font-size: clamp(26px, 1.9vw, 38px);
+			line-height: 1;
+			color: #45f1ff;
+			text-shadow: 0 0 16px rgba(69, 241, 255, .5);
+		}
+		.kpi.warn .kpi-value { color: #ffcf5b; text-shadow: 0 0 16px rgba(255, 207, 91, .38); }
+		.main {
+			display: grid;
+			grid-template-columns: minmax(0, 30fr) minmax(0, 40fr) minmax(0, 30fr);
+			gap: 14px;
+			min-width: 0;
+			min-height: 0;
+		}
+		.column {
+			display: grid;
+			gap: 14px;
+			min-width: 0;
+			min-height: 0;
+		}
+		.left { grid-template-rows: 44% 28% 28%; }
+		.center { grid-template-rows: 58% 42%; }
+		.right { grid-template-rows: 35% 32% 33%; }
+		.panel-title {
+			height: 42px;
+			display: flex;
+			align-items: center;
+			padding: 0 16px;
+			border-bottom: 1px solid rgba(88, 211, 255, .23);
+			color: #f1fdff;
+			font-size: 17px;
+			font-weight: 700;
+		}
+		.panel-title:before {
+			content: "";
+			width: 4px;
+			height: 18px;
+			margin-right: 9px;
+			background: #3be7ff;
+			box-shadow: 0 0 12px #3be7ff;
+		}
+		.chart {
+			width: 100%;
+			height: calc(100% - 42px);
+			min-width: 0;
+			min-height: 0;
+		}
+		.dual, .quad {
+			display: grid;
+			height: calc(100% - 42px);
+			min-width: 0;
+			min-height: 0;
+		}
+		.dual { grid-template-columns: 1fr 1fr; }
+		.quad { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
+		.dual .chart, .quad .chart { height: 100%; }
+		.table-wrap {
+			height: calc(100% - 42px);
+			padding: 10px 12px 12px;
+			overflow: hidden;
+		}
+		.scroll-body {
+			height: calc(100% - 30px);
+			overflow-y: auto;
+			scrollbar-width: thin;
+			scrollbar-color: rgba(77, 221, 255, .55) rgba(12, 52, 76, .38);
+		}
+		.scroll-body::-webkit-scrollbar { width: 6px; }
+		.scroll-body::-webkit-scrollbar-track { background: rgba(12, 52, 76, .38); }
+		.scroll-body::-webkit-scrollbar-thumb { background: rgba(77, 221, 255, .55); border-radius: 6px; }
+		.table-head,
+		.table-row {
+			display: grid;
+			gap: 10px;
+			align-items: center;
+			min-width: 0;
+		}
+		.stock-table .table-head,
+		.stock-table .table-row {
+			grid-template-columns: 1.15fr .75fr .9fr .8fr 70px .9fr;
+		}
+		.action-table .table-head,
+		.action-table .table-row {
+			grid-template-columns: 70px 1.15fr .9fr .75fr 62px 78px;
+		}
+		.expire-table .table-head,
+		.expire-table .table-row {
+			grid-template-columns: 1fr .85fr .75fr 62px .85fr;
+		}
+		.table-head {
+			height: 30px;
+			color: #7dbdd0;
+			font-size: 13px;
+			border-bottom: 1px solid rgba(96, 202, 255, .18);
+		}
+		.table-row {
+			height: 36px;
+			color: #dff8ff;
+			font-size: 13px;
+			border-bottom: 1px solid rgba(105, 199, 232, .08);
+		}
+		.cell {
+			min-width: 0;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+		}
+		.tag {
+			display: inline-flex;
+			align-items: center;
+			justify-content: center;
+			min-width: 54px;
+			height: 22px;
+			padding: 0 8px;
+			border: 1px solid rgba(79, 219, 255, .34);
+			background: rgba(22, 118, 147, .25);
+			color: #9ff4ff;
+			font-size: 12px;
+		}
+		.empty {
+			height: 100%;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			color: #6fa9bb;
+			font-size: 14px;
+		}
+	</style>
+</head>
+<body>
+<div class="screen">
+	<div class="shell">
+		<div class="header">
+			<div></div>
+			<div class="title">物料管理看板</div>
+			<div class="time" id="timeText"></div>
+		</div>
+
+		<div class="kpis">
+			<div class="kpi"><div class="kpi-label">物料总数</div><div class="kpi-value" id="materialCount">0</div></div>
+			<div class="kpi"><div class="kpi-label">库存批次数</div><div class="kpi-value" id="stockBatchCount">0</div></div>
+			<div class="kpi"><div class="kpi-label">当前库存总量</div><div class="kpi-value" id="stockTotal">0</div></div>
+			<div class="kpi"><div class="kpi-label">今日入库数量</div><div class="kpi-value" id="todayInbound">0</div></div>
+			<div class="kpi"><div class="kpi-label">今日出库数量</div><div class="kpi-value" id="todayOutbound">0</div></div>
+			<div class="kpi"><div class="kpi-label">今日来料报检</div><div class="kpi-value" id="todayReport">0</div></div>
+			<div class="kpi warn"><div class="kpi-label">待检来料</div><div class="kpi-value" id="pendingReport">0</div></div>
+			<div class="kpi warn"><div class="kpi-label">临期批次</div><div class="kpi-value" id="expireWarning">0</div></div>
+		</div>
+
+		<div class="main">
+			<div class="column left">
+				<div class="panel">
+					<div class="panel-title">近7日物料流转</div>
+					<div class="chart" id="trendChart"></div>
+				</div>
+				<div class="panel">
+					<div class="panel-title">来料质量状态</div>
+					<div class="quad">
+						<div class="chart" id="reportStatusChart"></div>
+						<div class="chart" id="checkStatusChart"></div>
+						<div class="chart" id="blpStatusChart"></div>
+						<div class="chart" id="llStatusChart"></div>
+					</div>
+				</div>
+				<div class="panel">
+					<div class="panel-title">入库类型</div>
+					<div class="chart" id="rkTypeChart"></div>
+				</div>
+			</div>
+
+			<div class="column center">
+				<div class="panel">
+					<div class="panel-title">当前库存明细</div>
+					<div class="table-wrap stock-table">
+						<div class="table-head"><div>物料</div><div>ERP</div><div>批次</div><div>库位</div><div>数量</div><div>过期日期</div></div>
+						<div class="scroll-body" id="stockList"></div>
+					</div>
+				</div>
+				<div class="panel">
+					<div class="panel-title">最新物料动态</div>
+					<div class="table-wrap action-table">
+						<div class="table-head"><div>类型</div><div>物料</div><div>批次</div><div>状态</div><div>数量</div><div>时间</div></div>
+						<div class="scroll-body" id="actionList"></div>
+					</div>
+				</div>
+			</div>
+
+			<div class="column right">
+				<div class="panel">
+					<div class="panel-title">库位库存TOP</div>
+					<div class="chart" id="locationTopChart"></div>
+				</div>
+				<div class="panel">
+					<div class="panel-title">物料库存TOP</div>
+					<div class="chart" id="materialTopChart"></div>
+				</div>
+				<div class="panel">
+					<div class="panel-title">临期库存预警</div>
+					<div class="table-wrap expire-table">
+						<div class="table-head"><div>物料</div><div>批次</div><div>库位</div><div>数量</div><div>过期日期</div></div>
+						<div class="scroll-body" id="expireList"></div>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+
+<script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
+<script src="${ctxStatic}/leaflet/js/echarts.min.js"></script>
+<script>
+	var charts = {};
+	var colors = ['#37e4ff', '#ffcf5a', '#65ffb4', '#ff7676', '#9c8cff', '#58a6ff', '#f28bff', '#b7e75b'];
+	var scrollTimers = {};
+	var scrollPaused = {};
+
+	function initCharts() {
+		['trendChart', 'reportStatusChart', 'checkStatusChart', 'blpStatusChart', 'llStatusChart', 'rkTypeChart', 'locationTopChart', 'materialTopChart'].forEach(function(id) {
+			charts[id] = echarts.init(document.getElementById(id));
+		});
+	}
+
+	function textStyle(size) {
+		return { color: '#a8d8e8', fontSize: size || 12 };
+	}
+
+	function render(data) {
+		var summary = data.summary || {};
+		['materialCount', 'stockBatchCount', 'stockTotal', 'todayInbound', 'todayOutbound', 'todayReport', 'pendingReport', 'expireWarning'].forEach(function(key) {
+			$('#' + key).text(summary[key] || 0);
+		});
+		renderTrend(data.days || [], data.trend || {});
+		setPie('reportStatusChart', '报检', data.reportStatus);
+		setPie('checkStatusChart', '检查', data.checkStatus);
+		setPie('blpStatusChart', '评审', data.blpStatus);
+		setPie('llStatusChart', '领料', data.llStatus);
+		setPie('rkTypeChart', '入库类型', data.rkType);
+		var locationTop = splitData(data.stockByLocation);
+		setBar('locationTopChart', locationTop.names, locationTop.values);
+		var materialTop = splitData(data.stockByMaterial);
+		setBar('materialTopChart', materialTop.names, materialTop.values);
+		renderStock(data.stockList || []);
+		renderActions(data.latestActions || []);
+		renderExpire(data.expireList || []);
+	}
+
+	function renderTrend(days, trend) {
+		charts.trendChart.setOption({
+			color: ['#3de7ff', '#ffcf5a', '#66ffb4', '#ff6f6f'],
+			tooltip: { trigger: 'axis' },
+			legend: { top: 10, right: 14, textStyle: textStyle(12), data: ['入库', '出库', '报检', '异常'] },
+			grid: { left: 48, right: 22, top: 48, bottom: 34 },
+			xAxis: { type: 'category', data: days, axisLabel: textStyle(12), axisLine: { lineStyle: { color: '#286982' } } },
+			yAxis: { type: 'value', axisLabel: textStyle(12), splitLine: { lineStyle: { color: 'rgba(95,196,230,.12)' } } },
+			series: [
+				{ name: '入库', type: 'bar', barWidth: 12, data: trend.inbound || [] },
+				{ name: '出库', type: 'bar', barWidth: 12, data: trend.outbound || [] },
+				{ name: '报检', type: 'line', smooth: true, data: trend.report || [] },
+				{ name: '异常', type: 'line', smooth: true, data: trend.abnormal || [] }
+			]
+		});
+	}
+
+	function setPie(id, title, data) {
+		charts[id].setOption({
+			color: colors,
+			tooltip: { trigger: 'item' },
+			title: { text: title, left: 'center', top: 'center', textStyle: { color: '#e9fbff', fontSize: 13, fontWeight: 400 } },
+			series: [{
+				type: 'pie',
+				radius: ['48%', '70%'],
+				center: ['50%', '50%'],
+				label: { color: '#cceff8', formatter: '{b}\n{c}', fontSize: 11 },
+				labelLine: { lineStyle: { color: 'rgba(180,235,255,.45)' } },
+				data: data || []
+			}]
+		});
+	}
+
+	function setBar(id, names, values) {
+		charts[id].setOption({
+			color: ['#39e7ff'],
+			grid: { left: 92, right: 22, top: 24, bottom: 28 },
+			tooltip: { trigger: 'axis' },
+			xAxis: { type: 'value', axisLabel: textStyle(11), splitLine: { lineStyle: { color: 'rgba(95,196,230,.12)' } } },
+			yAxis: { type: 'category', data: names, axisLabel: textStyle(11), axisLine: { lineStyle: { color: '#286982' } } },
+			series: [{ type: 'bar', data: values, barWidth: 12, itemStyle: { borderRadius: [0, 8, 8, 0] } }]
+		});
+	}
+
+	function splitData(list) {
+		var names = [], values = [];
+		(list || []).forEach(function(item) {
+			names.push(item.name);
+			values.push(item.value);
+		});
+		return { names: names, values: values };
+	}
+
+	function renderStock(list) {
+		renderRows('stockList', list, function(item) {
+			return '<div class="table-row">'
+				+ cell(item.material) + cell(item.erpSn) + cell(item.batchSn) + cell(item.kw) + cell(item.nums) + cell(item.expireDate)
+				+ '</div>';
+		}, '暂无库存数据');
+	}
+
+	function renderActions(list) {
+		renderRows('actionList', list, function(item) {
+			return '<div class="table-row">'
+				+ cell(item.type) + cell(item.material) + cell(item.batch) + '<div class="cell"><span class="tag">' + escapeHtml(item.state) + '</span></div>' + cell(item.nums) + cell(item.time)
+				+ '</div>';
+		}, '最近7天暂无物料动态');
+	}
+
+	function renderExpire(list) {
+		renderRows('expireList', list, function(item) {
+			return '<div class="table-row">'
+				+ cell(item.material) + cell(item.batchSn) + cell(item.kw) + cell(item.nums) + cell(item.expireDate)
+				+ '</div>';
+		}, '30天内暂无临期库存');
+	}
+
+	function renderRows(id, list, renderer, emptyText) {
+		var html = '';
+		(list || []).forEach(function(item) { html += renderer(item); });
+		$('#' + id).html(html || '<div class="empty">' + emptyText + '</div>');
+		startAutoScroll(id, 80);
+	}
+
+	function cell(value) {
+		return '<div class="cell" title="' + escapeHtml(value) + '">' + escapeHtml(value) + '</div>';
+	}
+
+	function startAutoScroll(id, speed) {
+		var box = document.getElementById(id);
+		if (scrollTimers[id]) {
+			clearInterval(scrollTimers[id]);
+			scrollTimers[id] = null;
+		}
+		if (!box || box.scrollHeight <= box.clientHeight) {
+			if (box) { box.scrollTop = 0; }
+			return;
+		}
+		scrollTimers[id] = setInterval(function() {
+			if (scrollPaused[id]) { return; }
+			if (box.scrollTop + box.clientHeight >= box.scrollHeight - 2) {
+				box.scrollTop = 0;
+			} else {
+				box.scrollTop += 1;
+			}
+		}, speed);
+	}
+
+	function escapeHtml(text) {
+		return String(text == null ? '' : text).replace(/[&<>"']/g, function(ch) {
+			return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[ch];
+		});
+	}
+
+	function loadData() {
+		$.post('${ctx}/mes/mesMaterial/materialScreenData', function(ret) {
+			if (ret && ret.result === 'true') {
+				render(ret.data || {});
+			}
+		});
+	}
+
+	function updateTime() {
+		var d = new Date();
+		var pad = function(n) { return n < 10 ? '0' + n : n; };
+		$('#timeText').text(d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()));
+	}
+
+	$(function() {
+		initCharts();
+		updateTime();
+		loadData();
+		setInterval(updateTime, 1000);
+		setInterval(loadData, 60000);
+		$('#stockList, #actionList, #expireList').on('mouseenter', function() { scrollPaused[this.id] = true; });
+		$('#stockList, #actionList, #expireList').on('mouseleave', function() { scrollPaused[this.id] = false; });
+		$(window).on('resize', function() {
+			Object.keys(charts).forEach(function(key) { charts[key].resize(); });
+			['stockList', 'actionList', 'expireList'].forEach(function(id) { startAutoScroll(id, 80); });
+		});
+	});
+</script>
+</body>
+</html>

+ 498 - 0
src/main/resources/views/modules/mes/mesQualityScreen.html

@@ -0,0 +1,498 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>质量管理看板</title>
+	<style>
+		* { box-sizing: border-box; }
+		html, body {
+			width: 100%;
+			height: 100%;
+			margin: 0;
+			overflow: hidden;
+			background: #06131f;
+			color: #d8fbff;
+			font-family: "Microsoft YaHei", Arial, sans-serif;
+		}
+		.quality-screen {
+			position: relative;
+			width: 100vw;
+			height: 100vh;
+			padding: clamp(12px, 1vw, 20px);
+			background:
+				linear-gradient(120deg, rgba(15, 92, 113, .18), transparent 36%),
+				linear-gradient(250deg, rgba(32, 81, 150, .18), transparent 40%),
+				linear-gradient(180deg, #071522 0%, #092034 52%, #06111d 100%);
+		}
+		.quality-screen:before {
+			content: "";
+			position: absolute;
+			inset: 0;
+			pointer-events: none;
+			background-image:
+				linear-gradient(rgba(97, 219, 255, .045) 1px, transparent 1px),
+				linear-gradient(90deg, rgba(97, 219, 255, .045) 1px, transparent 1px);
+			background-size: 44px 44px;
+		}
+		.shell {
+			position: relative;
+			z-index: 1;
+			display: grid;
+			grid-template-rows: 72px 104px 1fr;
+			gap: 14px;
+			width: 100%;
+			height: 100%;
+			min-width: 1200px;
+			min-height: 680px;
+		}
+		.header {
+			display: grid;
+			grid-template-columns: 1fr minmax(360px, 620px) 1fr;
+			align-items: center;
+		}
+		.title {
+			position: relative;
+			text-align: center;
+			font-size: clamp(30px, 2.25vw, 44px);
+			font-weight: 700;
+			color: #efffff;
+			text-shadow: 0 0 18px rgba(61, 226, 255, .85);
+			letter-spacing: 0;
+		}
+		.title:before,
+		.title:after {
+			content: "";
+			position: absolute;
+			top: 50%;
+			width: 170px;
+			height: 1px;
+			background: linear-gradient(90deg, transparent, rgba(83, 221, 255, .95));
+		}
+		.title:before { right: calc(100% + 18px); }
+		.title:after { left: calc(100% + 18px); transform: rotate(180deg); }
+		.time {
+			justify-self: end;
+			color: #8bd9ee;
+			font-size: 16px;
+			white-space: nowrap;
+		}
+		.kpis {
+			display: grid;
+			grid-template-columns: repeat(6, minmax(0, 1fr));
+			gap: 12px;
+		}
+		.kpi,
+		.panel {
+			position: relative;
+			border: 1px solid rgba(72, 206, 255, .42);
+			background: linear-gradient(180deg, rgba(9, 42, 68, .88), rgba(6, 24, 39, .78));
+			box-shadow: inset 0 0 24px rgba(52, 194, 228, .13), 0 0 20px rgba(0, 148, 210, .08);
+			overflow: hidden;
+		}
+		.kpi:before,
+		.panel:before {
+			content: "";
+			position: absolute;
+			inset: 0;
+			border: 1px solid rgba(158, 238, 255, .08);
+			pointer-events: none;
+		}
+		.kpi {
+			display: flex;
+			flex-direction: column;
+			justify-content: center;
+			padding: 14px 18px;
+			min-width: 0;
+		}
+		.kpi-label {
+			color: #9cc9d8;
+			font-size: 14px;
+			line-height: 20px;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+		}
+		.kpi-value {
+			margin-top: 8px;
+			font-family: Impact, "Arial Narrow", sans-serif;
+			font-size: clamp(30px, 2.2vw, 42px);
+			line-height: 1;
+			color: #45f1ff;
+			text-shadow: 0 0 16px rgba(69, 241, 255, .5);
+		}
+		.kpi.warn .kpi-value { color: #ffce58; text-shadow: 0 0 16px rgba(255, 206, 88, .36); }
+		.main {
+			display: grid;
+			grid-template-columns: minmax(0, 29fr) minmax(0, 42fr) minmax(0, 29fr);
+			gap: 14px;
+			min-height: 0;
+			min-width: 0;
+		}
+		.column {
+			display: grid;
+			gap: 14px;
+			min-height: 0;
+			min-width: 0;
+		}
+		.left { grid-template-rows: 45% 55%; }
+		.center { grid-template-rows: 58% 42%; }
+		.right { grid-template-rows: 36% 64%; }
+		.panel-title {
+			height: 42px;
+			display: flex;
+			align-items: center;
+			padding: 0 16px;
+			border-bottom: 1px solid rgba(88, 211, 255, .23);
+			color: #f0fdff;
+			font-size: 17px;
+			font-weight: 700;
+		}
+		.panel-title:before {
+			content: "";
+			width: 4px;
+			height: 18px;
+			margin-right: 9px;
+			background: #3be7ff;
+			box-shadow: 0 0 12px #3be7ff;
+		}
+		.chart {
+			width: 100%;
+			height: calc(100% - 42px);
+			min-height: 0;
+			min-width: 0;
+		}
+		.dual .chart,
+		.quad .chart {
+			height: 100%;
+		}
+		.dual {
+			display: grid;
+			grid-template-columns: 1fr 1fr;
+			height: calc(100% - 42px);
+			min-height: 0;
+			min-width: 0;
+		}
+		.quad {
+			display: grid;
+			grid-template-columns: 1fr 1fr;
+			grid-template-rows: 1fr 1fr;
+			height: calc(100% - 42px);
+			min-height: 0;
+			min-width: 0;
+		}
+		.table-wrap {
+			height: calc(100% - 42px);
+			padding: 10px 12px 12px;
+			overflow: hidden;
+		}
+		.table-head,
+		.table-row {
+			display: grid;
+			grid-template-columns: 72px 1.05fr .8fr 1.25fr 74px 82px;
+			gap: 10px;
+			align-items: center;
+			min-width: 0;
+		}
+		.table-head {
+			height: 30px;
+			color: #7abbd0;
+			font-size: 13px;
+			border-bottom: 1px solid rgba(96, 202, 255, .18);
+		}
+		.table-body {
+			height: calc(100% - 30px);
+			overflow-y: auto;
+			scrollbar-width: thin;
+			scrollbar-color: rgba(77, 221, 255, .55) rgba(12, 52, 76, .38);
+		}
+		.table-body::-webkit-scrollbar {
+			width: 6px;
+		}
+		.table-body::-webkit-scrollbar-track {
+			background: rgba(12, 52, 76, .38);
+		}
+		.table-body::-webkit-scrollbar-thumb {
+			background: rgba(77, 221, 255, .55);
+			border-radius: 6px;
+		}
+		.table-row {
+			height: 38px;
+			color: #d9f8ff;
+			font-size: 13px;
+			border-bottom: 1px solid rgba(105, 199, 232, .08);
+		}
+		.cell {
+			min-width: 0;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+		}
+		.tag {
+			display: inline-flex;
+			align-items: center;
+			justify-content: center;
+			min-width: 58px;
+			height: 22px;
+			padding: 0 8px;
+			border: 1px solid rgba(79, 219, 255, .34);
+			background: rgba(22, 118, 147, .25);
+			color: #9ff4ff;
+			font-size: 12px;
+		}
+		.empty {
+			height: 100%;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			color: #6fa9bb;
+			font-size: 14px;
+		}
+		@media (max-aspect-ratio: 15/9) {
+			.shell { min-width: 1100px; }
+			.main { grid-template-columns: minmax(0, 30fr) minmax(0, 40fr) minmax(0, 30fr); }
+			.table-head, .table-row { grid-template-columns: 66px 1fr .75fr 1fr 68px 76px; gap: 8px; }
+		}
+	</style>
+</head>
+<body>
+<div class="quality-screen">
+	<div class="shell">
+		<div class="header">
+			<div></div>
+			<div class="title">质量管理看板</div>
+			<div class="time" id="timeText"></div>
+		</div>
+
+		<div class="kpis">
+			<div class="kpi"><div class="kpi-label">今日过程检查</div><div class="kpi-value" id="checkCount">0</div></div>
+			<div class="kpi"><div class="kpi-label">今日首件检查</div><div class="kpi-value" id="firstCheckCount">0</div></div>
+			<div class="kpi warn"><div class="kpi-label">今日NG数量</div><div class="kpi-value" id="ngCount">0</div></div>
+			<div class="kpi"><div class="kpi-label">今日返修数量</div><div class="kpi-value" id="repairCount">0</div></div>
+			<div class="kpi warn"><div class="kpi-label">今日报废数量</div><div class="kpi-value" id="scrapCount">0</div></div>
+			<div class="kpi warn"><div class="kpi-label">今日未闭环返修</div><div class="kpi-value" id="pendingRepairCount">0</div></div>
+		</div>
+
+		<div class="main">
+			<div class="column left">
+				<div class="panel">
+					<div class="panel-title">近7日质量趋势</div>
+					<div class="chart" id="trendChart"></div>
+				</div>
+				<div class="panel">
+					<div class="panel-title">过程检查状态</div>
+					<div class="chart" id="checkStatusChart"></div>
+				</div>
+			</div>
+
+			<div class="column center">
+				<div class="panel">
+					<div class="panel-title">最新质量异常</div>
+					<div class="table-wrap">
+						<div class="table-head">
+							<div>类型</div><div>产品码</div><div>工位</div><div>内容</div><div>状态</div><div>时间</div>
+						</div>
+						<div class="table-body" id="issueList"></div>
+					</div>
+				</div>
+				<div class="panel">
+					<div class="panel-title">检查类型与首件状态</div>
+					<div class="dual">
+						<div class="chart" id="checkTypeChart"></div>
+						<div class="chart" id="firstStatusChart"></div>
+					</div>
+				</div>
+			</div>
+
+			<div class="column right">
+				<div class="panel">
+					<div class="panel-title">责任工位TOP</div>
+					<div class="chart" id="stationTopChart"></div>
+				</div>
+				<div class="panel">
+					<div class="panel-title">NG、返修与报废分析</div>
+					<div class="quad">
+						<div class="chart" id="ngTypeChart"></div>
+						<div class="chart" id="repairStateChart"></div>
+						<div class="chart" id="repairResultChart"></div>
+						<div class="chart" id="scrapReasonChart"></div>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+
+<script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
+<script src="${ctxStatic}/leaflet/js/echarts.min.js"></script>
+<script>
+	var charts = {};
+	var colors = ['#37e4ff', '#ffcf5a', '#65ffb4', '#ff7676', '#9c8cff', '#58a6ff', '#f28bff', '#b7e75b'];
+	var issueScrollTimer = null;
+	var issueScrollPaused = false;
+
+	function initCharts() {
+		['trendChart', 'checkStatusChart', 'checkTypeChart', 'firstStatusChart', 'stationTopChart', 'ngTypeChart', 'repairStateChart', 'repairResultChart', 'scrapReasonChart'].forEach(function(id) {
+			charts[id] = echarts.init(document.getElementById(id));
+		});
+	}
+
+	function chartTextStyle(size) {
+		return { color: '#a8d8e8', fontSize: size || 12 };
+	}
+
+	function setPie(id, title, data) {
+		charts[id].setOption({
+			color: colors,
+			tooltip: { trigger: 'item' },
+			title: { text: title, left: 'center', top: 'center', textStyle: { color: '#e9fbff', fontSize: 14, fontWeight: 400 } },
+			series: [{
+				type: 'pie',
+				radius: ['50%', '72%'],
+				center: ['50%', '50%'],
+				avoidLabelOverlap: true,
+				label: { color: '#cceff8', formatter: '{b}\n{c}' },
+				labelLine: { lineStyle: { color: 'rgba(180,235,255,.45)' } },
+				data: data || []
+			}]
+		});
+	}
+
+	function setBar(id, names, values, horizontal) {
+		var option = {
+			color: ['#39e7ff'],
+			grid: { left: horizontal ? 88 : 42, right: 22, top: 24, bottom: 34 },
+			tooltip: { trigger: 'axis' },
+			xAxis: { type: horizontal ? 'value' : 'category', data: horizontal ? null : names, axisLabel: chartTextStyle(11), axisLine: { lineStyle: { color: '#286982' } }, splitLine: { lineStyle: { color: 'rgba(95,196,230,.12)' } } },
+			yAxis: { type: horizontal ? 'category' : 'value', data: horizontal ? names : null, axisLabel: chartTextStyle(11), axisLine: { lineStyle: { color: '#286982' } }, splitLine: { lineStyle: { color: 'rgba(95,196,230,.12)' } } },
+			series: [{ type: 'bar', data: values, barWidth: horizontal ? 12 : 16, itemStyle: { borderRadius: horizontal ? [0, 8, 8, 0] : [8, 8, 0, 0] } }]
+		};
+		charts[id].setOption(option);
+	}
+
+	function render(data) {
+		var summary = data.summary || {};
+		['checkCount', 'firstCheckCount', 'ngCount', 'repairCount', 'scrapCount', 'pendingRepairCount'].forEach(function(key) {
+			$('#' + key).text(summary[key] || 0);
+		});
+
+		var trend = data.trend || {};
+		charts.trendChart.setOption({
+			color: ['#3de7ff', '#ffcf5a', '#66ffb4', '#ff6f6f'],
+			tooltip: { trigger: 'axis' },
+			legend: { top: 10, right: 14, textStyle: chartTextStyle(12), data: ['检查', 'NG', '返修', '报废'] },
+			grid: { left: 42, right: 22, top: 48, bottom: 34 },
+			xAxis: { type: 'category', data: data.days || [], axisLabel: chartTextStyle(12), axisLine: { lineStyle: { color: '#286982' } } },
+			yAxis: { type: 'value', axisLabel: chartTextStyle(12), splitLine: { lineStyle: { color: 'rgba(95,196,230,.12)' } } },
+			series: [
+				{ name: '检查', type: 'line', smooth: true, data: trend.checks || [] },
+				{ name: 'NG', type: 'line', smooth: true, data: trend.ng || [] },
+				{ name: '返修', type: 'bar', barWidth: 12, data: trend.repair || [] },
+				{ name: '报废', type: 'bar', barWidth: 12, data: trend.scrap || [] }
+			]
+		});
+
+		setPie('checkStatusChart', '检查状态', data.checkStatus);
+		setPie('checkTypeChart', '检查类型', data.checkType);
+		setPie('firstStatusChart', '首件状态', data.firstCheckStatus);
+		setPie('ngTypeChart', 'NG类型', data.ngType);
+		setPie('repairStateChart', '返修状态', data.repairState);
+		setPie('repairResultChart', '返修结果', data.repairResult);
+
+		var station = splitChartData(data.stationTop);
+		setBar('stationTopChart', station.names, station.values, true);
+		var scrap = splitChartData(data.scrapReasonTop);
+		setBar('scrapReasonChart', scrap.names, scrap.values, true);
+		renderIssues(data.latestIssues || []);
+	}
+
+	function splitChartData(list) {
+		var names = [], values = [];
+		(list || []).forEach(function(item) {
+			names.push(item.name);
+			values.push(item.value);
+		});
+		return { names: names, values: values };
+	}
+
+	function renderIssues(list) {
+		var $box = $('#issueList');
+		if (!list.length) {
+			$box.html('<div class="empty">最近7天暂无质量异常</div>');
+			return;
+		}
+		var html = '';
+		list.forEach(function(item) {
+			html += '<div class="table-row">'
+				+ '<div class="cell">' + escapeHtml(item.type) + '</div>'
+				+ '<div class="cell">' + escapeHtml(item.sn) + '</div>'
+				+ '<div class="cell">' + escapeHtml(item.station) + '</div>'
+				+ '<div class="cell">' + escapeHtml(item.content) + '</div>'
+				+ '<div class="cell"><span class="tag">' + escapeHtml(item.state) + '</span></div>'
+				+ '<div class="cell">' + escapeHtml(item.time) + '</div>'
+				+ '</div>';
+		});
+		$box.html(html);
+		startIssueAutoScroll();
+	}
+
+	function startIssueAutoScroll() {
+		var box = document.getElementById('issueList');
+		if (issueScrollTimer) {
+			clearInterval(issueScrollTimer);
+			issueScrollTimer = null;
+		}
+		if (!box || box.scrollHeight <= box.clientHeight) {
+			if (box) {
+				box.scrollTop = 0;
+			}
+			return;
+		}
+		issueScrollTimer = setInterval(function() {
+			if (issueScrollPaused) {
+				return;
+			}
+			if (box.scrollTop + box.clientHeight >= box.scrollHeight - 2) {
+				box.scrollTop = 0;
+			} else {
+				box.scrollTop += 1;
+			}
+		}, 80);
+	}
+
+	function escapeHtml(text) {
+		return String(text == null ? '' : text).replace(/[&<>"']/g, function(ch) {
+			return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[ch];
+		});
+	}
+
+	function loadData() {
+		$.post('${ctx}/mes/mesProduct/qualityScreenData', function(ret) {
+			if (ret && ret.result === 'true') {
+				render(ret.data || {});
+			}
+		});
+	}
+
+	function updateTime() {
+		var d = new Date();
+		var pad = function(n) { return n < 10 ? '0' + n : n; };
+		$('#timeText').text(d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()));
+	}
+
+	$(function() {
+		initCharts();
+		updateTime();
+		loadData();
+		setInterval(updateTime, 1000);
+		setInterval(loadData, 60000);
+		$('#issueList').on('mouseenter', function() { issueScrollPaused = true; });
+		$('#issueList').on('mouseleave', function() { issueScrollPaused = false; });
+		$(window).on('resize', function() {
+			Object.keys(charts).forEach(function(key) { charts[key].resize(); });
+			startIssueAutoScroll();
+		});
+	});
+</script>
+</body>
+</html>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 572 - 1200
src/main/resources/views/modules/mes/mesScreen5.html