Browse Source

Merge branch 'master' of http://121.36.46.135:3000/mes-ljz/mescloud

sunxianglong 2 months ago
parent
commit
ac153c1514
28 changed files with 1657 additions and 1036 deletions
  1. 14 0
      db/mes_product_screen.sql
  2. 15 0
      src/main/java/com/jeesite/modules/mes/dao/MesProductCncToolGeoDao.java
  3. 9 0
      src/main/java/com/jeesite/modules/mes/dao/MesProductRecordDao.java
  4. 15 0
      src/main/java/com/jeesite/modules/mes/dao/MesProductScreenDao.java
  5. 10 0
      src/main/java/com/jeesite/modules/mes/dao/MesRepairDao.java
  6. 104 0
      src/main/java/com/jeesite/modules/mes/entity/MesProductCncToolGeo.java
  7. 71 0
      src/main/java/com/jeesite/modules/mes/entity/MesProductScreen.java
  8. 46 0
      src/main/java/com/jeesite/modules/mes/service/MesProductCncToolGeoService.java
  9. 56 0
      src/main/java/com/jeesite/modules/mes/service/MesProductRecordService.java
  10. 52 0
      src/main/java/com/jeesite/modules/mes/service/MesProductScreenService.java
  11. 8 0
      src/main/java/com/jeesite/modules/mes/service/MesRepairService.java
  12. 25 21
      src/main/java/com/jeesite/modules/mes/web/MesMaterialRkController.java
  13. 155 0
      src/main/java/com/jeesite/modules/mes/web/MesProductCncToolGeoController.java
  14. 62 10
      src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java
  15. 105 0
      src/main/java/com/jeesite/modules/mes/web/MesProductScreenController.java
  16. 2 0
      src/main/resources/config/application.yml
  17. 4 0
      src/main/resources/mappings/modules/mes/MesProductCncToolGeoDao.xml
  18. 10 0
      src/main/resources/mappings/modules/mes/MesProductRecordDao.xml
  19. 4 0
      src/main/resources/mappings/modules/mes/MesProductScreenDao.xml
  20. 9 0
      src/main/resources/mappings/modules/mes/MesRepairDao.xml
  21. 60 0
      src/main/resources/views/modules/mes/mesProductCncToolGeoDetail.html
  22. 65 0
      src/main/resources/views/modules/mes/mesProductCncToolGeoList.html
  23. 73 0
      src/main/resources/views/modules/mes/mesProductScreenForm.html
  24. 69 0
      src/main/resources/views/modules/mes/mesProductScreenList.html
  25. 14 0
      src/main/resources/views/modules/mes/mesScrapList.html
  26. 256 246
      src/main/resources/views/modules/mes/mesScreen3.html
  27. 188 392
      src/main/resources/views/modules/mes/mesScreen4.html
  28. 156 367
      src/main/resources/views/modules/mes/mesScreen5.html

+ 14 - 0
db/mes_product_screen.sql

@@ -0,0 +1,14 @@
+-- 已有表 mes_product_screen 字段:people_num / machine_num / plan_output / create_by
+-- 为兼容 JeeSite 保存(字符串 id、时间戳),执行以下变更:
+
+ALTER TABLE `mes_product_screen`
+  MODIFY COLUMN `id` varchar(64) NOT NULL COMMENT '编号',
+  ADD COLUMN IF NOT EXISTS `create_date` datetime DEFAULT NULL COMMENT '创建时间' AFTER `create_by`,
+  ADD COLUMN IF NOT EXISTS `update_by` varchar(64) DEFAULT NULL COMMENT '更新者' AFTER `create_date`,
+  ADD COLUMN IF NOT EXISTS `update_date` datetime DEFAULT NULL COMMENT '更新时间' AFTER `update_by`;
+
+-- MySQL 5.7 若不支持 IF NOT EXISTS,可改用下面(已有列会报错可忽略):
+-- ALTER TABLE `mes_product_screen` MODIFY COLUMN `id` varchar(64) NOT NULL COMMENT '编号';
+-- ALTER TABLE `mes_product_screen` ADD COLUMN `create_date` datetime DEFAULT NULL COMMENT '创建时间' AFTER `create_by`;
+-- ALTER TABLE `mes_product_screen` ADD COLUMN `update_by` varchar(64) DEFAULT NULL COMMENT '更新者' AFTER `create_date`;
+-- ALTER TABLE `mes_product_screen` ADD COLUMN `update_date` datetime DEFAULT NULL COMMENT '更新时间' AFTER `update_by`;

+ 15 - 0
src/main/java/com/jeesite/modules/mes/dao/MesProductCncToolGeoDao.java

@@ -0,0 +1,15 @@
+package com.jeesite.modules.mes.dao;
+
+import com.jeesite.common.dao.CrudDao;
+import com.jeesite.common.mybatis.annotation.MyBatisDao;
+import com.jeesite.modules.mes.entity.MesProductCncToolGeo;
+
+/**
+ * CNC刀长半径采集DAO接口
+ * @author system
+ * @version 2026-08-11
+ */
+@MyBatisDao
+public interface MesProductCncToolGeoDao extends CrudDao<MesProductCncToolGeo> {
+
+}

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

@@ -47,4 +47,13 @@ public interface MesProductRecordDao extends CrudDao<MesProductRecord> {
             @Param("startTime") Date startTime,
             @Param("endTime") Date endTime);
 
+    /**
+     * 统计指定产线、工位前缀、时间段内 content=OK 的条数
+     */
+    Long countOkByLineAndOprnoPrefix(
+            @Param("lineSn") String lineSn,
+            @Param("oprnoPrefix") String oprnoPrefix,
+            @Param("startTime") Date startTime,
+            @Param("endTime") Date endTime);
+
 }

+ 15 - 0
src/main/java/com/jeesite/modules/mes/dao/MesProductScreenDao.java

@@ -0,0 +1,15 @@
+package com.jeesite.modules.mes.dao;
+
+import com.jeesite.common.dao.CrudDao;
+import com.jeesite.common.mybatis.annotation.MyBatisDao;
+import com.jeesite.modules.mes.entity.MesProductScreen;
+
+/**
+ * ´óÆÁչʾÅäÖÃDAO½Ó¿Ú
+ * @author mes
+ * @version 2026-08-12
+ */
+@MyBatisDao
+public interface MesProductScreenDao extends CrudDao<MesProductScreen> {
+
+}

+ 10 - 0
src/main/java/com/jeesite/modules/mes/dao/MesRepairDao.java

@@ -24,4 +24,14 @@ public interface MesRepairDao extends CrudDao<MesRepair> {
     List<Map<String, Object>> countGroupByItemTitle(
             @Param("sdate") String sdate,
             @Param("edate") String edate);
+
+    /**
+     * 按产线、时间段、责任工位号数值区间统计返工条数
+     */
+    Long countByLineAndDateAndOprnoNumRange(
+            @Param("lineSn") String lineSn,
+            @Param("sdate") String sdate,
+            @Param("edate") String edate,
+            @Param("fromNum") int fromNum,
+            @Param("toNum") int toNum);
 }

+ 104 - 0
src/main/java/com/jeesite/modules/mes/entity/MesProductCncToolGeo.java

@@ -0,0 +1,104 @@
+package com.jeesite.modules.mes.entity;
+
+import javax.validation.constraints.Size;
+
+import com.jeesite.common.entity.DataEntity;
+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 java.util.Date;
+
+/**
+ * CNC刀长半径采集Entity
+ * @author system
+ * @version 2026-08-11
+ */
+@Table(name="mes_product_cnc_tool_geo", alias="a", label="CNC刀长半径采集信息", columns={
+		@Column(name="id", attrName="id", label="id", isPK=true),
+		@Column(name="device_code", attrName="deviceCode", label="设备号", queryType=QueryType.LIKE),
+		@Column(name="tool_length", attrName="toolLength", label="刀长串", isQuery=false),
+		@Column(name="tool_radius", attrName="toolRadius", label="半径串", isQuery=false),
+		@Column(name="create_by", attrName="createBy", label="操作人", isUpdate=false, isQuery=false),
+		@Column(name="create_date", attrName="createDate", label="上传时间", isUpdate=false, isUpdateForce=true),
+	}, orderBy="a.create_date DESC"
+)
+public class MesProductCncToolGeo extends DataEntity<MesProductCncToolGeo> {
+
+	private static final long serialVersionUID = 1L;
+	private String deviceCode;
+	private String toolLength;
+	private String toolRadius;
+	/** 刀号(非表字段:列表展示 / 查询条件) */
+	private Integer toolNo;
+
+	@ExcelFields({
+		@ExcelField(title="设备号", attrName="deviceCode", align=Align.CENTER, sort=10),
+		@ExcelField(title="刀号", attrName="toolNo", align=Align.CENTER, sort=15),
+		@ExcelField(title="刀长", attrName="toolLength", align=Align.CENTER, sort=20),
+		@ExcelField(title="半径", attrName="toolRadius", align=Align.CENTER, sort=30),
+		@ExcelField(title="操作人", attrName="createBy", align=Align.CENTER, sort=40),
+		@ExcelField(title="上传时间", attrName="createDate", align=Align.CENTER, sort=50, dataFormat="yyyy-MM-dd HH:mm:ss"),
+	})
+	public MesProductCncToolGeo() {
+		this(null);
+	}
+
+	public MesProductCncToolGeo(String id) {
+		super(id);
+	}
+
+	@Size(min=0, max=64, message="设备号长度不能超过 64 个字符")
+	public String getDeviceCode() {
+		return deviceCode;
+	}
+
+	public void setDeviceCode(String deviceCode) {
+		this.deviceCode = deviceCode;
+	}
+
+	@Size(min=0, max=2000, message="刀长串长度不能超过 2000 个字符")
+	public String getToolLength() {
+		return toolLength;
+	}
+
+	public void setToolLength(String toolLength) {
+		this.toolLength = toolLength;
+	}
+
+	@Size(min=0, max=2000, message="半径串长度不能超过 2000 个字符")
+	public String getToolRadius() {
+		return toolRadius;
+	}
+
+	public void setToolRadius(String toolRadius) {
+		this.toolRadius = toolRadius;
+	}
+
+	public Integer getToolNo() {
+		return toolNo;
+	}
+
+	public void setToolNo(Integer toolNo) {
+		this.toolNo = toolNo;
+	}
+
+	public Date getCreateDate_gte() {
+		return sqlMap.getWhere().getValue("create_date", QueryType.GTE);
+	}
+
+	public void setCreateDate_gte(Date createDate) {
+		sqlMap.getWhere().and("create_date", QueryType.GTE, createDate);
+	}
+
+	public Date getCreateDate_lte() {
+		return sqlMap.getWhere().getValue("create_date", QueryType.LTE);
+	}
+
+	public void setCreateDate_lte(Date createDate) {
+		sqlMap.getWhere().and("create_date", QueryType.LTE, createDate);
+	}
+}

+ 71 - 0
src/main/java/com/jeesite/modules/mes/entity/MesProductScreen.java

@@ -0,0 +1,71 @@
+package com.jeesite.modules.mes.entity;
+
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.mybatis.annotation.Column;
+import com.jeesite.common.mybatis.annotation.Table;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+/**
+ * 大屏展示配置Entity(生产人数 / 设备数量 / 计划产量)
+ * 对应表 mes_product_screen
+ * @author mes
+ * @version 2026-08-12
+ */
+@Table(name="mes_product_screen", alias="a", label="大屏展示配置信息", columns={
+		@Column(name="id", attrName="id", label="id", isPK=true),
+		@Column(name="people_num", attrName="peopleNum", label="生产人数"),
+		@Column(name="machine_num", attrName="machineNum", label="设备数量"),
+		@Column(name="plan_output", attrName="planOutput", label="计划产量"),
+		@Column(name="create_by", attrName="createBy", label="创建人", isUpdate=false, isQuery=false),
+		@Column(name="create_date", attrName="createDate", label="创建日期", isUpdate=false, isQuery=false, isUpdateForce=true),
+		@Column(name="update_by", attrName="updateBy", label="update_by", isQuery=false),
+		@Column(name="update_date", attrName="updateDate", label="更新日期", isQuery=false, isUpdateForce=true),
+	}, orderBy="a.update_date DESC"
+)
+public class MesProductScreen extends DataEntity<MesProductScreen> {
+
+	private static final long serialVersionUID = 1L;
+	private String peopleNum;
+	private String machineNum;
+	private String planOutput;
+
+	public MesProductScreen() {
+		this(null);
+	}
+
+	public MesProductScreen(String id) {
+		super(id);
+	}
+
+	@NotBlank(message="生产人数不能为空")
+	@Size(min=0, max=255, message="生产人数长度不能超过 255 个字符")
+	public String getPeopleNum() {
+		return peopleNum;
+	}
+
+	public void setPeopleNum(String peopleNum) {
+		this.peopleNum = peopleNum;
+	}
+
+	@NotBlank(message="设备数量不能为空")
+	@Size(min=0, max=255, message="设备数量长度不能超过 255 个字符")
+	public String getMachineNum() {
+		return machineNum;
+	}
+
+	public void setMachineNum(String machineNum) {
+		this.machineNum = machineNum;
+	}
+
+	@NotBlank(message="计划产量不能为空")
+	@Size(min=0, max=255, message="计划产量长度不能超过 255 个字符")
+	public String getPlanOutput() {
+		return planOutput;
+	}
+
+	public void setPlanOutput(String planOutput) {
+		this.planOutput = planOutput;
+	}
+}

+ 46 - 0
src/main/java/com/jeesite/modules/mes/service/MesProductCncToolGeoService.java

@@ -0,0 +1,46 @@
+package com.jeesite.modules.mes.service;
+
+import java.util.List;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.mes.entity.MesProductCncToolGeo;
+import com.jeesite.modules.mes.dao.MesProductCncToolGeoDao;
+
+/**
+ * CNC刀长半径采集Service
+ * @author system
+ * @version 2026-08-11
+ */
+@Service
+public class MesProductCncToolGeoService extends CrudService<MesProductCncToolGeoDao, MesProductCncToolGeo> {
+
+	@Override
+	public MesProductCncToolGeo get(MesProductCncToolGeo mesProductCncToolGeo) {
+		return super.get(mesProductCncToolGeo);
+	}
+
+	@Override
+	public Page<MesProductCncToolGeo> findPage(MesProductCncToolGeo mesProductCncToolGeo) {
+		return super.findPage(mesProductCncToolGeo);
+	}
+
+	@Override
+	public List<MesProductCncToolGeo> findList(MesProductCncToolGeo mesProductCncToolGeo) {
+		return super.findList(mesProductCncToolGeo);
+	}
+
+	@Override
+	@Transactional
+	public void save(MesProductCncToolGeo mesProductCncToolGeo) {
+		super.save(mesProductCncToolGeo);
+	}
+
+	@Override
+	@Transactional
+	public void delete(MesProductCncToolGeo mesProductCncToolGeo) {
+		super.delete(mesProductCncToolGeo);
+	}
+}

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

@@ -1446,6 +1446,62 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 		return ret;
 	}
 
+	/**
+	 * 固定两班制当班产能:08:00–20:00 或 20:00–次日 08:00
+	 */
+	public Map<String, Integer> getFixedTwoShiftCapacityStats(String lineSn, List<String> oprnos) {
+		Map<String, Integer> ret = MapUtils.newHashMap();
+		if (StringUtils.isBlank(lineSn) || oprnos == null || oprnos.isEmpty()) {
+			return ret;
+		}
+		Calendar now = Calendar.getInstance();
+		int hour = now.get(Calendar.HOUR_OF_DAY);
+		Calendar startCal = Calendar.getInstance();
+		Calendar endCal = Calendar.getInstance();
+		startCal.set(Calendar.MINUTE, 0);
+		startCal.set(Calendar.SECOND, 0);
+		startCal.set(Calendar.MILLISECOND, 0);
+		endCal.set(Calendar.MINUTE, 0);
+		endCal.set(Calendar.SECOND, 0);
+		endCal.set(Calendar.MILLISECOND, 0);
+		if (hour >= 8 && hour < 20) {
+			startCal.set(Calendar.HOUR_OF_DAY, 8);
+			endCal.set(Calendar.HOUR_OF_DAY, 20);
+		} else if (hour >= 20) {
+			startCal.set(Calendar.HOUR_OF_DAY, 20);
+			endCal.add(Calendar.DAY_OF_MONTH, 1);
+			endCal.set(Calendar.HOUR_OF_DAY, 8);
+		} else {
+			endCal.set(Calendar.HOUR_OF_DAY, 8);
+			startCal.add(Calendar.DAY_OF_MONTH, -1);
+			startCal.set(Calendar.HOUR_OF_DAY, 20);
+		}
+		Date startTime = startCal.getTime();
+		Date endTime = endCal.getTime();
+		for (String oprno : oprnos) {
+			if (StringUtils.isBlank(oprno)) {
+				continue;
+			}
+			String formatOprno = CommonUitl.formatOprno(oprno.trim());
+			Long c = mesProductRecordDao.countHourCapacityByLineAndOprnoPrefix(
+					lineSn.trim(), formatOprno, startTime, endTime);
+			ret.put(formatOprno, c == null ? 0 : c.intValue());
+		}
+		return ret;
+	}
+
+	/**
+	 * 指定产线、工位前缀、时间段内 content=OK 的数量
+	 */
+	public int countOkByLineAndOprnoPrefix(String lineSn, String oprnoPrefix, Date startTime, Date endTime) {
+		if (StringUtils.isBlank(lineSn) || StringUtils.isBlank(oprnoPrefix) || startTime == null || endTime == null) {
+			return 0;
+		}
+		Long c = mesProductRecordDao.countOkByLineAndOprnoPrefix(
+				lineSn.trim(), CommonUitl.formatOprno(oprnoPrefix.trim()), startTime, endTime);
+		return c == null ? 0 : c.intValue();
+	}
+
 	private void fillHourlyCounts(List<Map<String, Object>> rows, int[] bucket) {
 		if (rows == null) {
 			return;

+ 52 - 0
src/main/java/com/jeesite/modules/mes/service/MesProductScreenService.java

@@ -0,0 +1,52 @@
+package com.jeesite.modules.mes.service;
+
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.ObjectUtils;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.mes.dao.MesProductScreenDao;
+import com.jeesite.modules.mes.entity.MesProductScreen;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * 大屏展示配置Service
+ * @author mes
+ * @version 2026-08-12
+ */
+@Service
+public class MesProductScreenService extends CrudService<MesProductScreenDao, MesProductScreen> {
+
+	@Override
+	public MesProductScreen get(MesProductScreen mesProductScreen) {
+		return super.get(mesProductScreen);
+	}
+
+	@Override
+	public Page<MesProductScreen> findPage(MesProductScreen mesProductScreen) {
+		return super.findPage(mesProductScreen);
+	}
+
+	@Override
+	public List<MesProductScreen> findList(MesProductScreen mesProductScreen) {
+		return super.findList(mesProductScreen);
+	}
+
+	@Override
+	@Transactional
+	public void save(MesProductScreen mesProductScreen) {
+		super.save(mesProductScreen);
+	}
+
+	@Override
+	@Transactional
+	public void delete(MesProductScreen mesProductScreen) {
+		super.delete(mesProductScreen);
+	}
+
+	public MesProductScreen getLatest() {
+		List<MesProductScreen> list = findList(new MesProductScreen());
+		return ObjectUtils.isEmpty(list) ? null : list.get(0);
+	}
+}

+ 8 - 0
src/main/java/com/jeesite/modules/mes/service/MesRepairService.java

@@ -134,4 +134,12 @@ public class MesRepairService extends CrudService<MesRepairDao, MesRepair> {
 				.and("create_date", QueryType.LT, edate);
 		return super.findCount(q);
 	}
+
+	/**
+	 * 统计指定产线、时间段内责任工位号在 [fromNum, toNum] 闭区间的返工条数
+	 */
+	public Long countByLineAndDateAndOprnoNumRange(String lineSn, String sdate, String edate, int fromNum, int toNum) {
+		Long c = dao.countByLineAndDateAndOprnoNumRange(lineSn, sdate, edate, fromNum, toNum);
+		return c == null ? 0L : c;
+	}
 }

+ 25 - 21
src/main/java/com/jeesite/modules/mes/web/MesMaterialRkController.java

@@ -230,24 +230,28 @@ public class MesMaterialRkController extends BaseController {
 		}
 		String unit=list.get(0).getUnit();
 
-		// 入库前校验:必须已报检且已来料检查,否则不允许入库
-		MesMaterialReport reportQuery = new MesMaterialReport();
-		reportQuery.setSn(materialCode.trim());
-		reportQuery.setStatus(DataEntity.STATUS_NORMAL);
-		MesMaterialReport report = mesMaterialReportService.findInfo(reportQuery);
-		if (report == null) {
-			resp.setResult(Global.FALSE);
-			resp.setMessage("该来料码未报检,无法入库!");
-			return resp;
-		}
-		MesMaterialCheckRecord checkQuery = new MesMaterialCheckRecord();
-		checkQuery.setSn(materialCode.trim());
-		checkQuery.setStatus(DataEntity.STATUS_NORMAL);
-		MesMaterialCheckRecord checkRecord = mesMaterialCheckRecordService.findInfo(checkQuery);
-		if (checkRecord == null) {
-			resp.setResult(Global.FALSE);
-			resp.setMessage("该来料码未检查,无法入库!");
-			return resp;
+		// 入库(无生产订单号)才校验报检/检查;领料传 orderId,不做该限制
+		boolean isInbound = StringUtils.isBlank(orderId);
+		MesMaterialCheckRecord checkRecord = null;
+		if (isInbound) {
+			MesMaterialReport reportQuery = new MesMaterialReport();
+			reportQuery.setSn(materialCode.trim());
+			reportQuery.setStatus(DataEntity.STATUS_NORMAL);
+			MesMaterialReport report = mesMaterialReportService.findInfo(reportQuery);
+			if (report == null) {
+				resp.setResult(Global.FALSE);
+				resp.setMessage("该来料码未报检,无法入库!");
+				return resp;
+			}
+			MesMaterialCheckRecord checkQuery = new MesMaterialCheckRecord();
+			checkQuery.setSn(materialCode.trim());
+			checkQuery.setStatus(DataEntity.STATUS_NORMAL);
+			checkRecord = mesMaterialCheckRecordService.findInfo(checkQuery);
+			if (checkRecord == null) {
+				resp.setResult(Global.FALSE);
+				resp.setMessage("该来料码未检查,无法入库!");
+				return resp;
+			}
 		}
 
 		HashMap<String, Object> data = new HashMap<>();
@@ -263,11 +267,11 @@ public class MesMaterialRkController extends BaseController {
 		data.put("unit", unit);                         // 单位
 
 		// 按来料码关联来料检查记录,带回合格/不合格数量,仅供 PDA 入库页初始显示(不覆盖手填,不参与 mesToErp)
-		String hgQty = StringUtils.defaultIfBlank(checkRecord.getHgQty(), "");
-		String bhgQty = StringUtils.defaultIfBlank(checkRecord.getBhgQty(), "");
+		String hgQty = checkRecord != null ? StringUtils.defaultIfBlank(checkRecord.getHgQty(), "") : "";
+		String bhgQty = checkRecord != null ? StringUtils.defaultIfBlank(checkRecord.getBhgQty(), "") : "";
 		data.put("hgQty", hgQty);   // 合格数量(来自检查记录)
 		data.put("bhgQty", bhgQty); // 不合格数量(来自检查记录)
-		if (orderId!=null){
+		if (StringUtils.isNotBlank(orderId)){
 			mesMaterialBl = mesMaterialLlService.findBlByMoAndItem(orderId, erpSn);
 			if (mesMaterialBl == null) {
 				throw new ServiceException(text("未找到对应备料信息!"));

+ 155 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductCncToolGeoController.java

@@ -0,0 +1,155 @@
+package com.jeesite.modules.mes.web;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+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 com.alibaba.druid.util.StringUtils;
+import com.jeesite.common.config.Global;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.entity.MesProductCncToolGeo;
+import com.jeesite.modules.mes.service.MesProductCncToolGeoService;
+
+/**
+ * CNC刀长半径采集Controller
+ * 库中一次提交存一条(# 拼串);列表一行对应一次提交,默认展示 1 号刀刀长/半径
+ * @author system
+ * @version 2026-08-11
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/mesProductCncToolGeo")
+public class MesProductCncToolGeoController extends BaseController {
+
+	@Autowired
+	private MesProductCncToolGeoService mesProductCncToolGeoService;
+
+	@ModelAttribute
+	public MesProductCncToolGeo get(String id, boolean isNewRecord) {
+		return mesProductCncToolGeoService.get(id, isNewRecord);
+	}
+
+	@RequiresPermissions("mes:mesProductCncToolGeo:view")
+	@RequestMapping(value = {"list", ""})
+	public String list(MesProductCncToolGeo mesProductCncToolGeo, Model model) {
+		model.addAttribute("mesProductCncToolGeo", mesProductCncToolGeo);
+		return "modules/mes/mesProductCncToolGeoList";
+	}
+
+	/**
+	 * 列表数据:一次提交一行;刀长/半径取指定刀号(默认 1 号)的段值
+	 */
+	@RequiresPermissions("mes:mesProductCncToolGeo:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MesProductCncToolGeo> listData(MesProductCncToolGeo mesProductCncToolGeo,
+			HttpServletRequest request, HttpServletResponse response) {
+		Integer displayToolNo = mesProductCncToolGeo.getToolNo();
+		if (displayToolNo == null || displayToolNo <= 0) {
+			displayToolNo = 1;
+		}
+		mesProductCncToolGeo.setToolNo(null);
+		mesProductCncToolGeo.setPage(new Page<>(request, response));
+		Page<MesProductCncToolGeo> page = mesProductCncToolGeoService.findPage(mesProductCncToolGeo);
+		if (page.getList() != null) {
+			for (MesProductCncToolGeo row : page.getList()) {
+				fillToolSegment(row, displayToolNo);
+			}
+		}
+		return page;
+	}
+
+	@RequiresPermissions("mes:mesProductCncToolGeo:view")
+	@RequestMapping(value = "detail")
+	public String detail(MesProductCncToolGeo mesProductCncToolGeo, Model model) {
+		model.addAttribute("mesProductCncToolGeo", mesProductCncToolGeo);
+		model.addAttribute("toolRows", parseToolGeo(mesProductCncToolGeo.getToolLength(),
+				mesProductCncToolGeo.getToolRadius()));
+		return "modules/mes/mesProductCncToolGeoDetail";
+	}
+
+	/**
+	 * 供工机客户端上传(匿名)
+	 */
+	@PostMapping(value = "saveToolGeo")
+	@ResponseBody
+	public String saveToolGeo(HttpServletRequest request) {
+		String deviceCode = request.getParameter("deviceCode");
+		String ucode = request.getParameter("ucode");
+		String toolLength = request.getParameter("toolLength");
+		String toolRadius = request.getParameter("toolRadius");
+		if (StringUtils.isEmpty(deviceCode) || toolLength == null || toolRadius == null) {
+			return renderResult(Global.FALSE, text("参数错误!"));
+		}
+		String[] lens = toolLength.split("#", -1);
+		String[] rads = toolRadius.split("#", -1);
+		if (lens.length == 0 || lens.length != rads.length) {
+			return renderResult(Global.FALSE, text("刀长/半径段数不一致!"));
+		}
+		boolean allEmpty = true;
+		for (int i = 0; i < lens.length; i++) {
+			if ((lens[i] != null && !lens[i].trim().isEmpty())
+					|| (rads[i] != null && !rads[i].trim().isEmpty())) {
+				allEmpty = false;
+				break;
+			}
+		}
+		if (allEmpty) {
+			return renderResult(Global.FALSE, text("刀长半径数据为空!"));
+		}
+
+		MesProductCncToolGeo entity = new MesProductCncToolGeo();
+		entity.setDeviceCode(deviceCode.trim());
+		entity.setToolLength(toolLength);
+		entity.setToolRadius(toolRadius);
+		entity.setCreateBy(StringUtils.isEmpty(ucode) ? "system" : ucode);
+		mesProductCncToolGeoService.save(entity);
+		return renderResult(Global.TRUE, text("操作成功!"));
+	}
+
+	/**
+	 * 列表展示:用指定刀号的段值覆盖 toolLength/toolRadius,并写入 toolNo
+	 */
+	private void fillToolSegment(MesProductCncToolGeo row, int toolNo) {
+		String lengthRaw = row.getToolLength();
+		String radiusRaw = row.getToolRadius();
+		String[] lens = lengthRaw == null ? new String[0] : lengthRaw.split("#", -1);
+		String[] rads = radiusRaw == null ? new String[0] : radiusRaw.split("#", -1);
+		int idx = toolNo - 1;
+		row.setToolNo(toolNo);
+		row.setToolLength(idx >= 0 && idx < lens.length ? nullToEmpty(lens[idx]) : "");
+		row.setToolRadius(idx >= 0 && idx < rads.length ? nullToEmpty(rads[idx]) : "");
+	}
+
+	/** 详情页:全部刀号段(含空) */
+	private List<Map<String, String>> parseToolGeo(String toolLength, String toolRadius) {
+		List<Map<String, String>> rows = new ArrayList<>();
+		String[] lens = toolLength == null ? new String[0] : toolLength.split("#", -1);
+		String[] rads = toolRadius == null ? new String[0] : toolRadius.split("#", -1);
+		int n = Math.max(lens.length, rads.length);
+		for (int i = 0; i < n; i++) {
+			Map<String, String> row = new HashMap<>();
+			row.put("toolNo", String.valueOf(i + 1));
+			row.put("length", i < lens.length ? nullToEmpty(lens[i]) : "");
+			row.put("radius", i < rads.length ? nullToEmpty(rads[i]) : "");
+			rows.add(row);
+		}
+		return rows;
+	}
+
+	private String nullToEmpty(String s) {
+		return s == null ? "" : s.trim();
+	}
+}

+ 62 - 10
src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java

@@ -442,7 +442,8 @@ public class MesProductRecordController extends BaseController {
 	}
 
 	/**
-	 * 当班产能:根据班次生产计划管理的当前班次时间段统计指定产线、各工位的产能
+	 * 当班产能:根据班次生产计划管理的当前班次时间段统计;
+	 * 传 fixedTwoShift=true 时改为固定两班 08:00–20:00 / 20:00–次日 08:00
 	 */
 	@RequestMapping(value = "shiftCapacityData")
 	@ResponseBody
@@ -450,6 +451,7 @@ public class MesProductRecordController extends BaseController {
 		CommonResp<Map<String, Integer>> resp = new CommonResp<>();
 		String lineSn = StringUtils.trimToEmpty(request.getParameter("lineSn"));
 		String oprnos = StringUtils.trimToEmpty(request.getParameter("oprnos"));
+		String fixedTwoShift = StringUtils.trimToEmpty(request.getParameter("fixedTwoShift"));
 		if (StringUtils.isBlank(lineSn) || StringUtils.isBlank(oprnos)) {
 			resp.setResult("false");
 			resp.setMessage("lineSn、oprnos 不能为空");
@@ -464,7 +466,10 @@ public class MesProductRecordController extends BaseController {
 					oprnoList.add(oprno);
 				}
 			}
-			resp.setData(mesProductRecordService.getCurrentShiftCapacityStats(lineSn, oprnoList));
+			boolean useFixed = "true".equalsIgnoreCase(fixedTwoShift) || "1".equals(fixedTwoShift);
+			resp.setData(useFixed
+					? mesProductRecordService.getFixedTwoShiftCapacityStats(lineSn, oprnoList)
+					: mesProductRecordService.getCurrentShiftCapacityStats(lineSn, oprnoList));
 			resp.setResult("true");
 		} catch (Exception e) {
 			logger.error(e.getMessage(), e);
@@ -476,25 +481,29 @@ public class MesProductRecordController extends BaseController {
 
 	/**
 	 * 近七日产品质检数据分析(按产线)
-	 * 产出:半成品入库管理(mes_inbound) 按产线、按天统计的入库数量
-	 * 不良:返工记录表(mes_repair) 按产线、按天统计的返工数量
-	 * 返回最近 7 天(含今天)的数据,labels/produced/defect 三个数组一一对应
+	 * 默认:产出=半成品入库,不良=返工全量
+	 * 可选 producedOprno:产出改为该工位 content=OK 按日计数
+	 * 可选 defectOprnoFrom/defectOprnoTo:不良改为责任工位号数值闭区间
 	 */
 	@RequestMapping(value = "weekQualityData")
 	@ResponseBody
 	public CommonResp<Map<String, Object>> weekQualityData(HttpServletRequest request) {
 		CommonResp<Map<String, Object>> resp = new CommonResp<>();
 		String lineSn = StringUtils.trimToEmpty(request.getParameter("lineSn"));
+		String producedOprno = StringUtils.trimToEmpty(request.getParameter("producedOprno"));
+		String defectFromStr = StringUtils.trimToEmpty(request.getParameter("defectOprnoFrom"));
+		String defectToStr = StringUtils.trimToEmpty(request.getParameter("defectOprnoTo"));
 		if (StringUtils.isBlank(lineSn)) {
 			resp.setResult("false");
 			resp.setMessage("lineSn 不能为空");
 			return resp;
 		}
 		try {
+			Integer defectFrom = parseOprnoNum(defectFromStr);
+			Integer defectTo = parseOprnoNum(defectToStr);
 			List<String> labels = new ArrayList<>();
 			List<Integer> produced = new ArrayList<>();
 			List<Integer> defect = new ArrayList<>();
-			SimpleDateFormat dayFmt = new SimpleDateFormat("yyyy-MM-dd");
 			Calendar cal = Calendar.getInstance();
 			cal.set(Calendar.HOUR_OF_DAY, 0);
 			cal.set(Calendar.MINUTE, 0);
@@ -512,10 +521,25 @@ public class MesProductRecordController extends BaseController {
 				int d = dayStartCal.get(Calendar.DAY_OF_MONTH);
 				labels.add(m + "月" + d + "日");
 
-				Long outNum = mesInboundService.countByLineAndDate(lineSn, dayStartStr, dayEndStr);
-				Long ngNum = mesRepairService.countByLineAndDate(lineSn, dayStartStr, dayEndStr);
-				produced.add(outNum == null ? 0 : outNum.intValue());
-				defect.add(ngNum == null ? 0 : ngNum.intValue());
+				int outNum;
+				if (StringUtils.isNotBlank(producedOprno)) {
+					outNum = mesProductRecordService.countOkByLineAndOprnoPrefix(
+							lineSn, producedOprno, dayStartCal.getTime(), dayEndCal.getTime());
+				} else {
+					Long inbound = mesInboundService.countByLineAndDate(lineSn, dayStartStr, dayEndStr);
+					outNum = inbound == null ? 0 : inbound.intValue();
+				}
+				int ngNum;
+				if (defectFrom != null && defectTo != null) {
+					Long ng = mesRepairService.countByLineAndDateAndOprnoNumRange(
+							lineSn, dayStartStr, dayEndStr, defectFrom, defectTo);
+					ngNum = ng == null ? 0 : ng.intValue();
+				} else {
+					Long ng = mesRepairService.countByLineAndDate(lineSn, dayStartStr, dayEndStr);
+					ngNum = ng == null ? 0 : ng.intValue();
+				}
+				produced.add(outNum);
+				defect.add(ngNum);
 
 				cal.add(Calendar.DAY_OF_MONTH, 1);
 			}
@@ -533,6 +557,34 @@ public class MesProductRecordController extends BaseController {
 		return resp;
 	}
 
+	/** 从 OP070 / OP100 解析工位数字;无法解析返回 null */
+	private static Integer parseOprnoNum(String oprno) {
+		if (StringUtils.isBlank(oprno)) {
+			return null;
+		}
+		String s = oprno.trim().toUpperCase();
+		if (s.startsWith("OP")) {
+			s = s.substring(2);
+		}
+		StringBuilder digits = new StringBuilder();
+		for (int i = 0; i < s.length(); i++) {
+			char c = s.charAt(i);
+			if (c >= '0' && c <= '9') {
+				digits.append(c);
+			} else {
+				break;
+			}
+		}
+		if (digits.length() == 0) {
+			return null;
+		}
+		try {
+			return Integer.parseInt(digits.toString());
+		} catch (NumberFormatException e) {
+			return null;
+		}
+	}
+
 	/**
 	 * 各月产量统计图(按产线)
 	 * 按月统计半成品入库管理(mes_inbound) 的入库数量,返回当年 1~12 月数据

+ 105 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductScreenController.java

@@ -0,0 +1,105 @@
+package com.jeesite.modules.mes.web;
+
+import com.jeesite.common.config.Global;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.entity.MesProductScreen;
+import com.jeesite.modules.mes.resp.CommonResp;
+import com.jeesite.modules.mes.service.MesProductScreenService;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 大屏展示配置Controller
+ * @author mes
+ * @version 2026-08-12
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/mesProductScreen")
+public class MesProductScreenController extends BaseController {
+
+	@Autowired
+	private MesProductScreenService mesProductScreenService;
+
+	@ModelAttribute
+	public MesProductScreen get(String id, boolean isNewRecord) {
+		return mesProductScreenService.get(id, isNewRecord);
+	}
+
+	@RequiresPermissions("mes:mesProductScreen:view")
+	@RequestMapping(value = {"list", ""})
+	public String list(MesProductScreen mesProductScreen, Model model) {
+		model.addAttribute("mesProductScreen", mesProductScreen);
+		return "modules/mes/mesProductScreenList";
+	}
+
+	@RequiresPermissions("mes:mesProductScreen:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MesProductScreen> listData(MesProductScreen mesProductScreen,
+			HttpServletRequest request, HttpServletResponse response) {
+		mesProductScreen.setPage(new Page<>(request, response));
+		return mesProductScreenService.findPage(mesProductScreen);
+	}
+
+	@RequiresPermissions("mes:mesProductScreen:view")
+	@RequestMapping(value = "form")
+	public String form(MesProductScreen mesProductScreen, Model model) {
+		model.addAttribute("mesProductScreen", mesProductScreen);
+		return "modules/mes/mesProductScreenForm";
+	}
+
+	@RequiresPermissions("mes:mesProductScreen:edit")
+	@PostMapping(value = "save")
+	@ResponseBody
+	public String save(@Validated MesProductScreen mesProductScreen) {
+		mesProductScreenService.save(mesProductScreen);
+		return renderResult(Global.TRUE, text("保存大屏展示配置成功!"));
+	}
+
+	@RequiresPermissions("mes:mesProductScreen:edit")
+	@RequestMapping(value = "delete")
+	@ResponseBody
+	public String delete(MesProductScreen mesProductScreen) {
+		mesProductScreenService.delete(mesProductScreen);
+		return renderResult(Global.TRUE, text("删除大屏展示配置成功!"));
+	}
+
+	@RequestMapping(value = "latestConfig")
+	@ResponseBody
+	public CommonResp<Map<String, Object>> latestConfig() {
+		CommonResp<Map<String, Object>> resp = new CommonResp<>();
+		MesProductScreen latest = mesProductScreenService.getLatest();
+		Map<String, Object> data = new HashMap<>();
+		data.put("peopleNums", parseIntSafe(latest == null ? null : latest.getPeopleNum()));
+		data.put("deviceNums", parseIntSafe(latest == null ? null : latest.getMachineNum()));
+		data.put("planNums", parseIntSafe(latest == null ? null : latest.getPlanOutput()));
+		resp.setData(data);
+		resp.setResult(Global.TRUE);
+		return resp;
+	}
+
+	private int parseIntSafe(String val) {
+		if (StringUtils.isBlank(val)) {
+			return 0;
+		}
+		try {
+			return Integer.parseInt(val.trim());
+		} catch (Exception e) {
+			return 0;
+		}
+	}
+}

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

@@ -706,6 +706,7 @@ shiro:
     ${adminPath}/mes/mesProductCncLife/saveLife = anon
     ${adminPath}/mes/mesProductCnc/saveBroken = anon
     ${adminPath}/mes/mesProductCncParam/batchsave = anon
+    ${adminPath}/mes/mesProductCncToolGeo/saveToolGeo = anon
     ${adminPath}/mes/mesProductRecord/pcdevice = anon
     ${adminPath}/mes/mesProductRecord/checkProduct = anon
     ${adminPath}/mes/mesProductRecord/editProduct = anon
@@ -750,6 +751,7 @@ shiro:
     ${adminPath}/mes/mesProductCnc/save= anon
     ${adminPath}/mes/mesProductCncLife/saveLife= anon
     ${adminPath}/mes/mesProductCncParam/batchsave= anon
+    ${adminPath}/mes/mesProductCncToolGeo/saveToolGeo= anon
     
     
   

+ 4 - 0
src/main/resources/mappings/modules/mes/MesProductCncToolGeoDao.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeesite.modules.mes.dao.MesProductCncToolGeoDao">
+</mapper>

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

@@ -75,4 +75,14 @@
           AND UPPER(TRIM(IFNULL(a.content, ''))) != 'UDF'
           AND a.oprno LIKE CONCAT(#{oprnoPrefix}, '%')
     </select>
+
+    <select id="countOkByLineAndOprnoPrefix" resultType="java.lang.Long">
+        SELECT COUNT(1)
+        FROM mes_product_record a
+        WHERE a.update_date <![CDATA[ >= ]]> #{startTime}
+          AND a.update_date <![CDATA[ < ]]> #{endTime}
+          AND a.line_sn = #{lineSn}
+          AND UPPER(TRIM(IFNULL(a.content, ''))) = 'OK'
+          AND a.oprno LIKE CONCAT(#{oprnoPrefix}, '%')
+    </select>
 </mapper>

+ 4 - 0
src/main/resources/mappings/modules/mes/MesProductScreenDao.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeesite.modules.mes.dao.MesProductScreenDao">
+</mapper>

+ 9 - 0
src/main/resources/mappings/modules/mes/MesRepairDao.xml

@@ -39,4 +39,13 @@
 		GROUP BY b.title
 		ORDER BY cnt DESC
 	</select>
+
+	<select id="countByLineAndDateAndOprnoNumRange" resultType="java.lang.Long">
+		SELECT COUNT(1)
+		FROM mes_repair a
+		WHERE a.line_sn = #{lineSn}
+		  AND a.create_date &gt;= #{sdate}
+		  AND a.create_date &lt; #{edate}
+		  AND CAST(SUBSTRING(a.oprno, 3) AS UNSIGNED) BETWEEN #{fromNum} AND #{toNum}
+	</select>
 </mapper>

+ 60 - 0
src/main/resources/views/modules/mes/mesProductCncToolGeoDetail.html

@@ -0,0 +1,60 @@
+<% layout('/layouts/default.html', {title: 'CNC刀长半径详情', libs: []}){ %>
+<style>
+	.table.table-bordered th.mpc-header { width: 140px; background: #f9f9f9; }
+	.tool-table th { background: #f5f5f5; text-align: center; }
+	.tool-table td { text-align: center; }
+</style>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header with-border">
+			<div class="box-title">
+				<i class="fa icon-eye"></i> ${text('CNC刀长半径详情')}
+			</div>
+		</div>
+		<div class="box-body">
+			<div class="form-unit">${text('基本信息')}</div>
+			<table class="table table-bordered">
+				<tr>
+					<th class="mpc-header">${text('设备号')}</th>
+					<td>${mesProductCncToolGeo.deviceCode}</td>
+					<th class="mpc-header">${text('操作人')}</th>
+					<td>${mesProductCncToolGeo.createBy}</td>
+				</tr>
+				<tr>
+					<th class="mpc-header">${text('上传时间')}</th>
+					<td colspan="3">${@DateUtils.formatDateTime(mesProductCncToolGeo.createDate)}</td>
+				</tr>
+			</table>
+
+			<div class="form-unit">${text('按刀号解析(第N段=N号刀,#分隔)')}</div>
+			<% if(toolRows == null || toolRows.~size == 0){ %>
+				<div class="alert alert-info" style="margin:0;">暂无刀具数据</div>
+			<% } else { %>
+				<table class="table table-bordered tool-table">
+					<thead>
+						<tr>
+							<th style="width:100px;">${text('刀号')}</th>
+							<th>${text('刀长 $TC_DP3')}</th>
+							<th>${text('半径 $TC_DP6')}</th>
+						</tr>
+					</thead>
+					<tbody>
+						<% for(var row in toolRows){ %>
+						<tr>
+							<td>${row.toolNo}</td>
+							<td>${row.length != '' ? row.length : '-'}</td>
+							<td>${row.radius != '' ? row.radius : '-'}</td>
+						</tr>
+						<% } %>
+					</tbody>
+				</table>
+			<% } %>
+		</div>
+		<div class="box-footer">
+			<button type="button" class="btn btn-sm btn-default" onclick="js.closeCurrentTabPage()">
+				<i class="fa fa-reply-all"></i> ${text('关 闭')}
+			</button>
+		</div>
+	</div>
+</div>
+<% } %>

+ 65 - 0
src/main/resources/views/modules/mes/mesProductCncToolGeoList.html

@@ -0,0 +1,65 @@
+<% layout('/layouts/default.html', {title: 'CNC刀长半径管理', libs: ['dataGrid']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa icon-notebook"></i> ${text('CNC刀长半径管理')}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="#" class="btn btn-default" id="btnSearch" title="${text('查询')}"><i class="fa fa-filter"></i> ${text('查询')}</a>
+				<a href="#" class="btn btn-default" id="btnSetting" title="${text('设置')}"><i class="fa fa-navicon"></i></a>
+			</div>
+		</div>
+		<div class="box-body">
+			<#form:form id="searchForm" model="${mesProductCncToolGeo}" action="${ctx}/mes/mesProductCncToolGeo/listData" method="post" class="form-inline hide"
+					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<div class="form-group">
+					<label class="control-label">${text('设备号')}:</label>
+					<div class="control-inline">
+						<#form:input path="deviceCode" maxlength="64" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('刀号')}:</label>
+					<div class="control-inline">
+						<#form:input path="toolNo" maxlength="10" class="form-control width-90" placeholder="默认1"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('上传时间')}:</label>
+					<div class="control-inline">
+						<#form:input path="createDate_gte" readonly="true" maxlength="20" class="form-control laydate width-date"
+							dataFormat="date" data-type="date" data-format="yyyy-MM-dd" data-done="createDate_lte.click()"/>
+						&nbsp;-&nbsp;
+						<#form:input path="createDate_lte" readonly="true" maxlength="20" class="form-control laydate width-date"
+							dataFormat="date" data-type="date" data-format="yyyy-MM-dd"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<button type="submit" class="btn btn-primary btn-sm">${text('查询')}</button>
+					<button type="reset" class="btn btn-default btn-sm">${text('重置')}</button>
+				</div>
+			</#form:form>
+			<table id="dataGrid"></table>
+			<div id="dataGridPage"></div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("设备号")}', name:'deviceCode', index:'a.device_code', width:100, align:"center", frozen:true},
+		{header:'${text("刀号")}', name:'toolNo', index:'toolNo', width:80, align:"center"},
+		{header:'${text("刀长")}', name:'toolLength', index:'toolLength', width:120, align:"center"},
+		{header:'${text("半径")}', name:'toolRadius', index:'toolRadius', width:120, align:"center"},
+		{header:'${text("操作人")}', name:'createBy', index:'a.create_by', width:100, align:"center"},
+		{header:'${text("上传时间")}', name:'createDate', index:'a.create_date', width:160, align:"center"},
+		{header:'${text("操作")}', name:'actions', width:80, align:"center", formatter: function(val, obj, row){
+			return '<a href="${ctx}/mes/mesProductCncToolGeo/detail?id='+row.id+'" class="btnList" title="${text("全部刀号详情")}"><i class="fa fa-fw fa-eye"></i></a>';
+		}}
+	],
+	ajaxSuccess: function(data){}
+});
+</script>

+ 73 - 0
src/main/resources/views/modules/mes/mesProductScreenForm.html

@@ -0,0 +1,73 @@
+<% layout('/layouts/default.html', {title: '大屏展示配置', libs: ['validate']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header with-border">
+			<div class="box-title">
+				<i class="fa icon-note"></i> ${text(mesProductScreen.isNewRecord ? '新增大屏展示配置' : '编辑大屏展示配置')}
+			</div>
+			<div class="box-tools pull-right">
+				<button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-minus"></i></button>
+			</div>
+		</div>
+		<#form:form id="inputForm" model="${mesProductScreen}" action="${ctx}/mes/mesProductScreen/save" method="post" class="form-horizontal">
+			<div class="box-body">
+				<div class="form-unit">${text('基本信息')}</div>
+				<#form:hidden path="id"/>
+				<div class="row">
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4" title="">
+								<span class="required ">*</span> ${text('生产人数')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-8">
+								<#form:input path="peopleNum" maxlength="255" class="form-control required digits"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4" title="">
+								<span class="required ">*</span> ${text('设备数量')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-8">
+								<#form:input path="machineNum" maxlength="255" class="form-control required digits"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4" title="">
+								<span class="required ">*</span> ${text('计划产量')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-8">
+								<#form:input path="planOutput" maxlength="255" class="form-control required digits"/>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div class="box-footer">
+				<div class="row">
+					<div class="col-sm-offset-2 col-sm-10">
+						<% if (hasPermi('mes:mesProductScreen:edit')){ %>
+							<button type="submit" class="btn btn-sm btn-primary" id="btnSubmit"><i class="fa fa-check"></i> ${text('保 存')}</button>&nbsp;
+						<% } %>
+						<button type="button" class="btn btn-sm btn-default" id="btnCancel" onclick="js.closeCurrentTabPage()"><i class="fa fa-reply-all"></i> ${text('关 闭')}</button>
+					</div>
+				</div>
+			</div>
+		</#form:form>
+	</div>
+</div>
+<% } %>
+<script>
+$("#inputForm").validate({
+	submitHandler: function(form){
+		js.ajaxSubmitForm($(form), function(data){
+			js.showMessage(data.message);
+			if(data.result == Global.TRUE){
+				js.closeCurrentTabPage(function(contentWindow){
+					contentWindow.page();
+				});
+			}
+		}, "json");
+    }
+});
+</script>

+ 69 - 0
src/main/resources/views/modules/mes/mesProductScreenList.html

@@ -0,0 +1,69 @@
+<% layout('/layouts/default.html', {title: '大屏展示配置', libs: ['dataGrid']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa icon-notebook"></i> ${text('大屏展示配置')}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="#" class="btn btn-default" id="btnSearch" title="${text('查询')}"><i class="fa fa-filter"></i> ${text('查询')}</a>
+				<a href="${ctx}/mes/mesProduct/screen" target="_blank" class="btn btn-default"><i class="fa fa-desktop"></i> ${text('总表大屏')}</a>
+				<% if(hasPermi('mes:mesProductScreen:edit')){ %>
+					<a href="${ctx}/mes/mesProductScreen/form" class="btn btn-default btnTool" title="${text('新增大屏展示配置')}"><i class="fa fa-plus"></i> ${text('新增')}</a>
+				<% } %>
+				<a href="#" class="btn btn-default" id="btnSetting" title="${text('设置')}"><i class="fa fa-navicon"></i></a>
+			</div>
+		</div>
+		<div class="box-body">
+			<#form:form id="searchForm" model="${mesProductScreen}" action="${ctx}/mes/mesProductScreen/listData" method="post" class="form-inline hide"
+					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<div class="form-group">
+					<label class="control-label">${text('生产人数')}:</label>
+					<div class="control-inline">
+						<#form:input path="peopleNum" maxlength="255" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('设备数量')}:</label>
+					<div class="control-inline">
+						<#form:input path="machineNum" maxlength="255" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('计划产量')}:</label>
+					<div class="control-inline">
+						<#form:input path="planOutput" maxlength="255" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<button type="submit" class="btn btn-primary btn-sm">${text('查询')}</button>
+					<button type="reset" class="btn btn-default btn-sm">${text('重置')}</button>
+				</div>
+			</#form:form>
+			<table id="dataGrid"></table>
+			<div id="dataGridPage"></div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("生产人数")}', name:'peopleNum', index:'a.people_num', width:150, align:"center"},
+		{header:'${text("设备数量")}', name:'machineNum', index:'a.machine_num', width:150, align:"center"},
+		{header:'${text("计划产量")}', name:'planOutput', index:'a.plan_output', width:150, align:"center"},
+		{header:'${text("操作人")}', name:'createBy', index:'a.create_by', width:150, align:"center"},
+		{header:'${text("操作")}', name:'actions', width:120, formatter: function(val, obj, row, act){
+			var actions = [];
+			//<% if(hasPermi('mes:mesProductScreen:edit')){ %>
+				actions.push('<a href="${ctx}/mes/mesProductScreen/form?id='+row.id+'" class="btnList" title="${text("编辑大屏展示配置")}"><i class="fa fa-pencil"></i></a>&nbsp;');
+				actions.push('<a href="${ctx}/mes/mesProductScreen/delete?id='+row.id+'" class="btnList" title="${text("删除大屏展示配置")}" data-confirm="${text("确认要删除该大屏展示配置吗?")}"><i class="fa fa-trash-o"></i></a>&nbsp;');
+			//<% } %>
+			return actions.join('');
+		}}
+	],
+	ajaxSuccess: function(data){
+	}
+});
+</script>

+ 14 - 0
src/main/resources/views/modules/mes/mesScrapList.html

@@ -30,6 +30,20 @@
 					</div>
 				</div>
 				<div class="form-group">
+					<label class="control-label">${text('起始时间')}:</label>
+					<div class="control-inline">
+						<#form:input path="updateDate_gte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
+							dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm" data-done="updateDate_lte.click()"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('结束时间')}:</label>
+					<div class="control-inline">
+						<#form:input path="updateDate_lte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
+							dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm"/>
+					</div>
+				</div>
+				<div class="form-group">
 					<button type="submit" class="btn btn-primary btn-sm">${text('查询')}</button>
 					<button type="reset" class="btn btn-default btn-sm">${text('重置')}</button>
 				</div>

+ 256 - 246
src/main/resources/views/modules/mes/mesScreen3.html

@@ -101,20 +101,25 @@
 			bottom: 0;
 		}
 
-		/* ===== 左侧三列面板 ===== */
+		/* ===== 左侧三列面板(容器铺满主体,便于按名称重排到与 screen3/4/5 对齐) ===== */
 		.mes3-left {
 			position: absolute;
-			left: 30px;
-			top: 58px;
-			width: 440px;
-			bottom: 15px;
+			left: 0;
+			top: 0;
+			right: 0;
+			bottom: 0;
+			width: auto;
+			pointer-events: none;
+		}
+		.mes3-left .mes3-panel {
+			pointer-events: auto;
 		}
 
 		/* 通用面板样式 */
 		.mes3-panel {
 			position: absolute;
 			left: 0;
-			right: 0;
+			right: auto;
 			background-image: url("${ctxStatic}/screen/imgs/ljrZ1.png");
 			background-size: 100% 100%;
 			background-repeat: no-repeat;
@@ -138,58 +143,55 @@
 			overflow: hidden;
 		}
 
-		/* 左侧面板 - 工位状态 */
+		/* 近七日产量折线图 → 右上(对齐前屏「总览」高度区) */
 		.mes3-panel-left1 {
-			top: 0;
-			height: 301px;
+			left: 1450px;
+			top: 58px;
+			width: 440px;
+			height: 290px;
+			background-image: url("${ctxStatic}/screen/imgs/ljrY1.png");
 		}
 		.mes3-panel-left1 .panel-body {
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			flex-wrap: wrap;
-			gap: 5px;
-			padding: 5px;
+			display: block;
+			padding: 8px 10px;
 		}
 		.mes3-panel-left1 .panel-body img {
 			width: 70px;
 			height: auto;
 		}
 
-		/* 左侧面板 - 设备状态 */
+		/* 近七日产品抽检/质检 → 左下(对齐前屏质检区) */
 		.mes3-panel-left2 {
-			top: 322px;
-			height: 300px;
+			left: 30px;
+			top: 508px;
+			width: 440px;
+			height: 460px;
 		}
 		.mes3-panel-left2 .panel-body {
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			flex-wrap: wrap;
-			gap: 5px;
-			padding: 5px;
+			display: block;
+			padding: 8px 10px;
 		}
 		.mes3-panel-left2 .panel-body img {
 			width: 70px;
 			height: auto;
 		}
 
-		/* 左侧面板 - 实时曲线 */
+		/* 不良类型柱状图:隐藏(左边第三个) */
 		.mes3-panel-left3 {
-			top: 643px;
-			height: 300px;
+			display: none;
 		}
 		.mes3-panel-left3 .panel-body {
 			padding: 5px;
 		}
 
-		/* ===== 中间区域 ===== */
+		/* ===== 中间区域(中下表隐藏后加高) ===== */
 		.mes3-center {
 			position: absolute;
 			left: 490px;
 			top: 58px;
 			width: 940px;
-			height: 620px;
+			bottom: 15px;
+			height: auto;
 		}
 
 		/* 中间Logo区域 */
@@ -261,16 +263,9 @@
 			display: none;
 		}
 
-		/* ===== 中间下方大面板 - 设备故障统计分析 ===== */
+		/* ===== 中间下方大面板 - 设备故障统计分析:隐藏(中间第二个) ===== */
 		.mes3-panel-center-bottom {
-			position: absolute;
-			left: 490px;
-			top: 697px;
-			width: 940px;
-			height: 301px;
-			background-image: url("${ctxStatic}/screen/imgs/ljrY2.png");
-			background-size: 100% 100%;
-			background-repeat: no-repeat;
+			display: none;
 		}
 		.mes3-panel-center-bottom .panel-header {
 			height: 45px;
@@ -291,16 +286,9 @@
 		}
 
 		/* ===== 右侧面板区域 ===== */
-		/* 右侧面板1 - 达成率数据分析(右上角小面板)*/
+		/* 达成率数据分析:隐藏(右边第一个) */
 		.mes3-panel-right1 {
-			position: absolute;
-			left: 1450px;
-			top: 58px;
-			width: 440px;
-			height: 369px;
-			background-image: url("${ctxStatic}/screen/imgs/ljrY1.png");
-			background-size: 100% 100%;
-			background-repeat: no-repeat;
+			display: none;
 		}
 		.mes3-panel-right1 .panel-header {
 			height: 45px;
@@ -361,13 +349,13 @@
 			margin-left: 5px;
 		}
 
-		/* 右侧面板2 - 每小时上下线数据分析 */
+		/* 每小时上下线 → 右下(对齐前屏同名表) */
 		.mes3-panel-right2 {
 			position: absolute;
 			left: 1450px;
-			top: 446px;
+			top: 508px;
 			width: 440px;
-			height: 301px;
+			height: 460px;
 			background-image: url("${ctxStatic}/screen/imgs/ljrY2.png");
 			background-size: 100% 100%;
 			background-repeat: no-repeat;
@@ -390,14 +378,14 @@
 			padding: 10px;
 		}
 
-		/* 右侧面板3 - 报警信息 */
+		/* 设备报警 → 左上(对齐前屏报警信息区) */
 		.mes3-panel-right3 {
 			position: absolute;
-			left: 1450px;
-			top: 767px;
+			left: 30px;
+			top: 58px;
 			width: 440px;
-			height: 231px;
-			background-image: url("${ctxStatic}/screen/imgs/ljrY3.png");
+			height: 430px;
+			background-image: url("${ctxStatic}/screen/imgs/ljrZ1.png");
 			background-size: 100% 100%;
 			background-repeat: no-repeat;
 		}
@@ -564,6 +552,33 @@
 		/* ===== 报警信息 ===== */
 		.mes3-alert-list {
 			padding-top: 5px;
+			padding-right: 4px;
+			height: 100%;
+			overflow-y: auto;
+			scrollbar-width: thin;
+			scrollbar-color: rgba(0, 200, 255, 0.45) rgba(0, 40, 80, 0.25);
+		}
+		.mes3-alert-list::-webkit-scrollbar {
+			width: 6px;
+		}
+		.mes3-alert-list::-webkit-scrollbar-track {
+			background: rgba(0, 40, 80, 0.25);
+			border-radius: 3px;
+		}
+		.mes3-alert-list::-webkit-scrollbar-thumb {
+			background: linear-gradient(180deg, rgba(0, 200, 255, 0.55), rgba(77, 201, 245, 0.35));
+			border-radius: 3px;
+		}
+		.mes3-alert-list::-webkit-scrollbar-thumb:hover {
+			background: rgba(0, 230, 255, 0.7);
+		}
+		.mes3-alert-list::-webkit-scrollbar-button {
+			display: none;
+			width: 0;
+			height: 0;
+		}
+		.mes3-alert-list::-webkit-scrollbar-corner {
+			background: transparent;
 		}
 		.mes3-alert-item {
 			display: flex;
@@ -630,14 +645,14 @@
 		<div class="mes3-left">
 			<!-- 面板1: 近七日产量折线图 -->
 			<div class="mes3-panel mes3-panel-left1">
-				<div class="panel-header">近七日产量折线图</div>
+				<div class="panel-header">近七日产量分析</div>
 				<div class="panel-body">
 					<canvas id="chartLeft1" class="mes3-chart"></canvas>
 				</div>
 			</div>
-			<!-- 面板2: 近七日产品检验盘点 -->
+			<!-- 面板2: 近七日产品质检(对齐前屏同名逻辑) -->
 			<div class="mes3-panel mes3-panel-left2">
-				<div class="panel-header">近七日产品抽检盘点</div>
+				<div class="panel-header">近七日产品质检数据分析</div>
 				<div class="panel-body">
 					<canvas id="chartLeft2" class="mes3-chart"></canvas>
 				</div>
@@ -755,7 +770,7 @@
 
 		<!-- 右侧面板3: 报警信息 -->
 		<div class="mes3-panel-right3">
-			<div class="panel-header">设备报警(类型)</div>
+			<div class="panel-header">产线阶段报警信息</div>
 			<div class="panel-body" style="padding: 0 15px;">
 				<div id="alertList" class="mes3-alert-list"></div>
 			</div>
@@ -786,13 +801,12 @@
 	var mockData = {};
 
 	/**
-	 * 总览页:汇总 mesScreen4 / 5 / 6 三条产线每小时上下线 OK 数(与各分屏工位、lineSn 一致)
+	 * 总览页:全产线汇总,上线 OP060 / 下线 OP400(lineSn 空=不过滤产线)
 	 */
-	var HOURLY_API_CFG_LIST = [
-		{ lineSn: 'MP', onlineOprno: 'OP120', offlineOprno: 'OP190' },
-		{ lineSn: 'XT', onlineOprno: 'OP010',  offlineOprno: 'OP180' },
-		{ lineSn: 'ZP', onlineOprno: 'OP190', offlineOprno: 'OP250' }
-	];
+	var HOURLY_API_CFG = { lineSn: '', onlineOprno: 'OP060', offlineOprno: 'OP400' };
+	var OP_ONLINE = 'OP060';
+	var OP_OFFLINE = 'OP400';
+	var QUALITY_LINE_LIST = ['MP', 'XT', 'ZP'];
 
 	function buildHourLabels24() {
 		var hourLabels = [];
@@ -803,68 +817,31 @@
 	}
 
 	function updateHourlyUpDownByApi() {
-		var upSum = [], downSum = [], h;
-		for (h = 0; h < 24; h++) {
-			upSum[h] = 0;
-			downSum[h] = 0;
-		}
-		var total = HOURLY_API_CFG_LIST.length;
-		var finished = 0;
-
-		function applyAggregatedChart() {
-			var maxVal = 10;
-			for (h = 0; h < 24; h++) {
-				maxVal = Math.max(maxVal, upSum[h], downSum[h]);
-			}
-			mockData.hourlyData = {
-				labels: buildHourLabels24(),
-				up: upSum,
-				down: downSum,
-				max: maxVal
-			};
-			drawAllCharts();
-		}
-
-		function onOneRequestDone() {
-			finished++;
-			if (finished >= total) {
-				applyAggregatedChart();
-			}
-		}
-
-		function mergeHourlyResp(d) {
-			if (!d || !d.up || !d.down || d.up.length < 24 || d.down.length < 24) {
-				return;
-			}
-			for (h = 0; h < 24; h++) {
-				upSum[h] += Number(d.up[h]) || 0;
-				downSum[h] += Number(d.down[h]) || 0;
-			}
-		}
-
-		for (var i = 0; i < HOURLY_API_CFG_LIST.length; i++) {
-			(function(cfg) {
-				$.ajax({
-					url: '${ctx}/mes/mesProduct/hourlyUpDownOkData',
-					type: 'POST',
-					dataType: 'json',
-					data: {
-						lineSn: cfg.lineSn,
-						onlineOprno: cfg.onlineOprno,
-						offlineOprno: cfg.offlineOprno
-					},
-					success: function(resp) {
-						if (resp && resp.result === 'true' && resp.data) {
-							mergeHourlyResp(resp.data);
-						}
-						onOneRequestDone();
-					},
-					error: function() {
-						onOneRequestDone();
+		$.ajax({
+			url: '${ctx}/mes/mesProduct/hourlyUpDownOkData',
+			type: 'POST',
+			dataType: 'json',
+			data: {
+				lineSn: HOURLY_API_CFG.lineSn,
+				onlineOprno: HOURLY_API_CFG.onlineOprno,
+				offlineOprno: HOURLY_API_CFG.offlineOprno
+			},
+			success: function(resp) {
+				if (resp && resp.result === 'true' && resp.data) {
+					var d = resp.data;
+					if (!d.up || !d.down || d.up.length < 24 || d.down.length < 24) {
+						return;
 					}
-				});
-			})(HOURLY_API_CFG_LIST[i]);
-		}
+					mockData.hourlyData = {
+						labels: buildHourLabels24(),
+						up: d.up,
+						down: d.down,
+						max: typeof d.max === 'number' ? d.max : 100
+					};
+					drawAllCharts();
+				}
+			}
+		});
 	}
 
 	function generateMockData() {
@@ -1499,56 +1476,26 @@
 		});
 	}
 
-	function updateProductionPeopleByApi() {
-		$.ajax({
-			url: '${ctx}/sys/empUser/listData',
-			type: 'POST',
-			dataType: 'json',
-			data: {
-				pageNo: 1,
-				pageSize: 5000,
-				'employee.office.officeName': '生产部'
-			},
-			success: function(resp) {
-				var peopleCount = 0;
-				var users = extractList(resp);
-				for (var i = 0; i < users.length; i++) {
-					var u = users[i] || {};
-					var office = (u.employee && u.employee.office) ? u.employee.office : {};
-					var officeName = office.officeName || '';
-					var normalizedOfficeName = (officeName + '').replace(/\s/g, '');
-					if (normalizedOfficeName === '生产部' || /[/\\>]+生产部$/.test(normalizedOfficeName)) {
-						peopleCount++;
-					}
-				}
-				mockData.cards = mockData.cards || {};
-				mockData.cards.productionPeople = peopleCount;
-				var target = document.getElementById('productionPeople');
-				if (target) target.textContent = peopleCount;
-			}
-		});
-	}
-
-	function updateDeviceCountByApi() {
+	/** 总览三卡:生产人数 / 设备数量 / 计划产量 ← mesProductScreen 最新配置 */
+	function updateScreenConfigByApi() {
 		$.ajax({
-			url: '${ctx}/mes/mesDevice/listData',
+			url: '${ctx}/mes/mesProductScreen/latestConfig',
 			type: 'POST',
 			dataType: 'json',
-			data: {
-				pageNo: 1,
-				pageSize: 1
-			},
 			success: function(resp) {
-				var deviceCount = 0;
-				if (resp && typeof resp.count === 'number') {
-					deviceCount = resp.count;
-				} else {
-					deviceCount = extractList(resp).length;
-				}
+				if (!resp || resp.result !== 'true' || !resp.data) return;
+				var d = resp.data;
 				mockData.cards = mockData.cards || {};
-				mockData.cards.deviceCount = deviceCount;
-				var target = document.getElementById('deviceCount');
-				if (target) target.textContent = deviceCount;
+				mockData.cards.productionPeople = Number(d.peopleNums) || 0;
+				mockData.cards.deviceCount = Number(d.deviceNums) || 0;
+				mockData.cards.planProduction = Number(d.planNums) || 0;
+				var elPeople = document.getElementById('productionPeople');
+				var elDevice = document.getElementById('deviceCount');
+				var elPlan = document.getElementById('planProduction');
+				if (elPeople) elPeople.textContent = mockData.cards.productionPeople;
+				if (elDevice) elDevice.textContent = mockData.cards.deviceCount;
+				if (elPlan) elPlan.textContent = mockData.cards.planProduction;
+				syncAchieveRateWithPlan();
 			}
 		});
 	}
@@ -1583,28 +1530,12 @@
 		drawAllCharts();
 	}
 
-	function updateLatestPlanProductionByApi() {
-		$.ajax({
-			url: '${ctx}/mes/mesPlan/latestPlanNums',
-			type: 'POST',
-			dataType: 'json',
-			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
-				var planNums = Number(resp.data.nums) || 0;
-				mockData.cards = mockData.cards || {};
-				mockData.cards.planProduction = planNums;
-				syncAchieveRateWithPlan();
-			}
-		});
-	}
-
-	function isOpr240Family(oprno) {
+	function isOprOfflineFamily(oprno) {
 		var op = (oprno || '').replace(/\s/g, '').toUpperCase();
-		if (op.length < 5 || op.indexOf('OP240') !== 0) return false;
-		if (op.length === 5) return op === 'OP240';
-		return /^OP240[A-Z0-9_-]+$/.test(op);
+		var prefix = OP_OFFLINE;
+		if (op.length < prefix.length || op.indexOf(prefix) !== 0) return false;
+		if (op.length === prefix.length) return op === prefix;
+		return new RegExp('^' + prefix + '[A-Z0-9_-]+$').test(op);
 	}
 
 	function isRecordContentOk(content) {
@@ -1638,20 +1569,65 @@
 		return t >= day.startMs && t <= day.endMs;
 	}
 
-	// 综合大屏 - 当天返修数量(mes_repair) / 报废数量(mes_scrap)
+	function extractPageCount(resp) {
+		if (!resp) return 0;
+		if (typeof resp.count === 'number') return resp.count;
+		if (resp.data && typeof resp.data.count === 'number') return resp.data.count;
+		var list = extractList(resp);
+		return list.length;
+	}
+
+	// 中间卡:返修数量 ← mesRepair/listData;报废数量 ← mesScrap/listData(按当天 updateDate)
 	function updateRepairScrapCountByApi() {
+		var dayRange = getCalendarDayUpdateDateRange();
+		var pending = 2;
+		var repairCount = 0;
+		var scrapCount = 0;
+
+		function done() {
+			pending--;
+			if (pending > 0) return;
+			mockData.cards = mockData.cards || {};
+			mockData.cards.repairCount = repairCount;
+			mockData.cards.scrapCount = scrapCount;
+			renderTextData();
+		}
+
 		$.ajax({
-			url: '${ctx}/mes/mesProduct/dashboardCount',
+			url: '${ctx}/mes/mesRepair/listData',
 			type: 'POST',
 			dataType: 'json',
+			data: {
+				pageNo: 1,
+				pageSize: 1,
+				updateDate_gte: dayRange.gte,
+				updateDate_lte: dayRange.lte
+			},
 			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
-				mockData.cards = mockData.cards || {};
-				mockData.cards.repairCount = Number(resp.data.repair) || 0;
-				mockData.cards.scrapCount = Number(resp.data.scrap) || 0;
-				renderTextData();
+				repairCount = extractPageCount(resp);
+				done();
+			},
+			error: function() {
+				done();
+			}
+		});
+
+		$.ajax({
+			url: '${ctx}/mes/mesScrap/listData',
+			type: 'POST',
+			dataType: 'json',
+			data: {
+				pageNo: 1,
+				pageSize: 1,
+				updateDate_gte: dayRange.gte,
+				updateDate_lte: dayRange.lte
+			},
+			success: function(resp) {
+				scrapCount = extractPageCount(resp);
+				done();
+			},
+			error: function() {
+				done();
 			}
 		});
 	}
@@ -1675,26 +1651,77 @@
 		});
 	}
 
-	// 近七日产品抽检盘点:按天统计 mes_quality 的 OK(合格) / NG(不良) 次数
+	// 近七日产品质检:产出=OP400 OK,不良=返工 OP070–OP400,按产线汇总
 	function updateQualityInspectByApi() {
-		$.ajax({
-			url: '${ctx}/mes/mesProduct/qualityInspectData',
-			type: 'POST',
-			dataType: 'json',
-			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) return;
-				var d = resp.data;
-				mockData.qualityInspect = {
-					labels: d.labels || [],
-					produced: d.qualified || [], // 沿用原 produced 字段渲染(柱状图代码不动)
-					defect: d.defect || [],
-					max: Number(d.max) || 10
-				};
-				drawAllCharts();
+		var labels = null;
+		var producedSum = null;
+		var defectSum = null;
+		var total = QUALITY_LINE_LIST.length;
+		var finished = 0;
+
+		function finishOne() {
+			finished++;
+			if (finished < total) return;
+			if (!producedSum || !defectSum) return;
+			var maxV = 10;
+			var i;
+			for (i = 0; i < producedSum.length; i++) {
+				if (Number(producedSum[i]) > maxV) maxV = Number(producedSum[i]);
 			}
-		});
+			for (i = 0; i < defectSum.length; i++) {
+				if (Number(defectSum[i]) > maxV) maxV = Number(defectSum[i]);
+			}
+			mockData.qualityInspect = {
+				labels: labels || [],
+				produced: producedSum,
+				defect: defectSum,
+				max: Math.ceil(maxV * 1.15) || 10
+			};
+			drawAllCharts();
+		}
+
+		for (var li = 0; li < QUALITY_LINE_LIST.length; li++) {
+			(function(lineSn) {
+				$.ajax({
+					url: '${ctx}/mes/mesProductRecord/weekQualityData',
+					type: 'POST',
+					dataType: 'json',
+					data: {
+						lineSn: lineSn,
+						producedOprno: OP_OFFLINE,
+						defectOprnoFrom: 'OP070',
+						defectOprnoTo: OP_OFFLINE
+					},
+					success: function(resp) {
+						if (resp && resp.result === 'true' && resp.data) {
+							var d = resp.data;
+							if (!labels) labels = d.labels || [];
+							var p = d.produced || [];
+							var df = d.defect || [];
+							if (!producedSum) {
+								producedSum = [];
+								defectSum = [];
+								for (var i = 0; i < 7; i++) {
+									producedSum[i] = 0;
+									defectSum[i] = 0;
+								}
+							}
+							for (var j = 0; j < 7; j++) {
+								producedSum[j] += Number(p[j]) || 0;
+								defectSum[j] += Number(df[j]) || 0;
+							}
+						}
+						finishOne();
+					},
+					error: function() {
+						finishOne();
+					}
+				});
+			})(QUALITY_LINE_LIST[li]);
+		}
 	}
 
+	/** 实际产量:OP400 当天完成(OK)数 */
 	function updateActualProductionByApi() {
 		var dayRange = getCalendarDayUpdateDateRange();
 		var pageSize = 3000;
@@ -1704,8 +1731,9 @@
 
 		function applyActualProductionCount(n) {
 			mockData.cards = mockData.cards || {};
-			mockData.cards.actualProduction = n;
+			mockData.cards.actualProduction = Number(n) || 0;
 			syncAchieveRateWithPlan();
+			renderTextData();
 		}
 
 		function fetchPage() {
@@ -1716,9 +1744,8 @@
 				data: {
 					pageNo: pageNo,
 					pageSize: pageSize,
-					oprno: 'OP240',
+					oprno: OP_OFFLINE,
 					content: 'OK',
-					lineSn: 'XT',
 					updateDate_gte: dayRange.gte,
 					updateDate_lte: dayRange.lte
 				},
@@ -1726,7 +1753,7 @@
 					var list = extractList(resp);
 					for (var i = 0; i < list.length; i++) {
 						var r = list[i] || {};
-						if (isOpr240Family(r.oprno) && isRecordContentOk(r.content) && isRecordWithinToday(r, dayRange) && String(r.lineSn || r.line_sn || '').toUpperCase() === 'XT') {
+						if (isOprOfflineFamily(r.oprno) && isRecordContentOk(r.content) && isRecordWithinToday(r, dayRange)) {
 							totalOk++;
 						}
 					}
@@ -1745,11 +1772,10 @@
 		fetchPage();
 	}
 
-	function matchesXtOp240OkRecord(r) {
+	function matchesOfflineOkRecord(r) {
 		var t = parseRecordDateMs(r.updateDate || r.update_date);
 		if (isNaN(t)) return false;
-		if (!isOpr240Family(r.oprno) || !isRecordContentOk(r.content)) return false;
-		if (String(r.lineSn || r.line_sn || '').toUpperCase() !== 'XT') return false;
+		if (!isOprOfflineFamily(r.oprno) || !isRecordContentOk(r.content)) return false;
 		return true;
 	}
 
@@ -1790,9 +1816,8 @@
 				data: {
 					pageNo: pageNo,
 					pageSize: pageSize,
-					oprno: 'OP240',
+					oprno: OP_OFFLINE,
 					content: 'OK',
-					lineSn: 'XT',
 					updateDate_gte: gte,
 					updateDate_lte: lte
 				},
@@ -1801,7 +1826,7 @@
 					var j;
 					for (j = 0; j < list.length; j++) {
 						var r = list[j] || {};
-						if (!matchesXtOp240OkRecord(r)) continue;
+						if (!matchesOfflineOkRecord(r)) continue;
 						var t = parseRecordDateMs(r.updateDate || r.update_date);
 						var key = dateKeyFromMs(t, padFn);
 						if (Object.prototype.hasOwnProperty.call(counts, key)) {
@@ -1836,12 +1861,9 @@
 	}
 
 	function refreshData() {
-		// 暂未修改后台接口,直接使用 mock 数据
 		generateMockData();
 		renderTextData();
-		updateProductionPeopleByApi();
-		updateDeviceCountByApi();
-		updateLatestPlanProductionByApi();
+		updateScreenConfigByApi();
 		updateActualProductionByApi();
 		updateProductTrend7DaysByApi();
 		updateDeviceAlarmAlertsByApi();
@@ -1861,27 +1883,15 @@
 	}
 
 	function drawAllCharts() {
-		// 左1:近七日产量折线图
+		// 近七日产量(右上)
 		var d1 = mockData.productTrend;
 		if (d1) drawLineChart('chartLeft1', d1.data, d1.labels, d1.max, d1.min, '#00C8FF');
 
-		// 左2:近七日产品检验盘点
+		// 近七日产品质检(左下)
 		var d2 = mockData.qualityInspect;
 		if (d2) drawDoubleBarChart('chartLeft2', d2.produced, d2.defect, d2.labels, d2.max);
 
-		// 左3:不良类型柱状图
-		var d3 = mockData.defectType;
-		if (d3) drawBarChart('chartLeft3', d3.data, d3.labels, d3.max);
-
-		// 中下:设备故障统计分析 - 近七日设备故障分析
-		var d4 = mockData.faultAnalysis;
-		if (d4) drawBarLineChart('chartCenterBottom', d4.faultTime, d4.faultRate, d4.labels, d4.maxTime, d4.maxRate);
-
-		// 右1:达成率双线图
-		var d5 = mockData.achieveRate;
-		if (d5) drawDualLineChart('chartRight1', d5.target, d5.actual, d5.labels, d5.max, d5.min);
-
-		// 右2:每小时上下线数据
+		// 每小时上下线(右下)
 		var d6 = mockData.hourlyData;
 		if (d6) drawAreaLineChart('chartRight2', d6.up, d6.down, d6.labels, d6.max);
 	}

+ 188 - 392
src/main/resources/views/modules/mes/mesScreen4.html

@@ -40,34 +40,34 @@
 		.mes4-title {
 			position: absolute;
 			left: 50%;
-			top: 20px;
+			top: 18px;
 			transform: translateX(-50%);
-			font-size: 38px;
+			font-size: 42px;
 			font-weight: bold;
 			color: #FFFFFF;
-			letter-spacing: 3px;
+			letter-spacing: 4px;
 			text-shadow: 0 0 10px rgba(77, 201, 245, 0.6);
 			white-space: nowrap;
 		}
 		.mes4-datetime {
 			position: absolute;
-			top: 32px;
+			top: 30px;
 			right: 50px;
 			text-align: right;
 		}
 		.mes4-datetime .mes4-date {
 			display: inline-block;
-			font-size: 18px;
+			font-size: 20px;
 			color: #FFFFFF;
-			line-height: 28px;
+			line-height: 30px;
 		}
 		.mes4-datetime .mes4-time {
 			display: inline-block;
 			margin-left: 14px;
-			font-size: 22px;
+			font-size: 26px;
 			color: #FFFFFF;
 			font-weight: bold;
-			line-height: 28px;
+			line-height: 30px;
 		}
 
 		/* ===== 面板通用 - 使用自定义类名避免与 Bootstrap 的 .panel 冲突 ===== */
@@ -79,107 +79,111 @@
 			box-shadow: none;
 		}
 		.mes4-panel .mes4-ptitle {
-			height: 42px;
-			line-height: 42px;
+			height: 48px;
+			line-height: 48px;
 			text-align: center;
-			font-size: 18px;
+			font-size: 22px;
 			font-weight: bold;
 			color: #4DC9F5;
-			letter-spacing: 1px;
+			letter-spacing: 2px;
 			background: transparent;
 		}
 		.mes4-panel .mes4-pbody {
 			position: absolute;
-			top: 42px;
+			top: 48px;
 			left: 0;
 			right: 0;
 			bottom: 0;
-			padding: 5px 10px;
+			padding: 8px 14px;
 			background: transparent;
 		}
 		.chart { width: 100%; height: 100%; display: block; }
 
-		/* ===== 位置定义 (1920 × 1080 画布) ===== */
-		/* 左列: x=30, 宽=430 */
-		.p-alert    { left: 40px;  top: 130px; width: 430px; height: 290px; }
-		.p-quality  { left: 40px;  top: 455px; width: 430px; height: 265px; }
-		.p-product  { left: 40px;  top: 763px; width: 430px; height: 306px; }
+		/* ===== 位置:左右对齐,中间表按 5 行收短 ===== */
+		.p-alert    { left: 36px;  top: 118px; width: 440px; height: 430px; }
+		.p-quality  { left: 36px;  top: 568px; width: 440px; height: 460px; }
+		.p-product  { display: none; }
 
-		/* 中列: x=480, 宽=960 */
-		.p-table    { left: 480px; top: 130px; width: 960px; height: 575px; }
-		.p-month    { left: 480px; top: 725px; width: 960px; height: 306px; }
+		.p-table    { left: 496px; top: 118px; width: 928px; height: 560px; }
+		.p-month    { display: none; }
 
-		/* 右列: right=30, 宽=430 */
 		.p-overview { right: 30px; top: 130px; width: 430px; height: 290px; }
-		.p-achieve  { right: 40px; top: 455px; width: 430px; height: 265px; }
-		.p-hourly   { right: 40px; top: 763px; width: 430px; height: 306px; }
+		.p-achieve  { display: none; }
+		.p-hourly   { right: 36px; top: 568px; width: 440px; height: 460px; }
 
-		/* 阶段报警信息 - 按模板比例调整 */
-		.p-alert .mes4-ptitle {
+		.p-alert .mes4-ptitle,
+		.p-overview .mes4-ptitle,
+		.p-quality .mes4-ptitle,
+		.p-hourly .mes4-ptitle {
 			height: 40px;
 			line-height: 40px;
 			font-size: 16px;
-			color: #4DC9F5;
-			font-weight: bold;
-			letter-spacing: 1px;
 		}
-		.p-alert .mes4-pbody  { top: 40px; padding: 0; }
-		/* 总览标题 */
-		.p-overview .mes4-ptitle { height: 40px; line-height: 40px; font-size: 16px; }
-		.p-overview .mes4-pbody  { top: 40px; }
+		.p-alert .mes4-pbody { top: 48px; padding: 0 6px 8px; }
+		.p-overview .mes4-pbody { top: 40px; padding: 0; }
+		.p-table .mes4-pbody { top: 8px; padding: 8px 28px 12px; }
+		.p-quality .mes4-pbody,
+		.p-hourly .mes4-pbody { top: 72px; }
 
 		/* ===== 报警信息 ===== */
 		.alert-head {
 			display: flex;
 			align-items: center;
-			height: 36px;
-			padding: 0 22px;
+			height: 42px;
+			padding: 0 20px;
 			color: #4DC9F5;
-			font-size: 14px;
+			font-size: 16px;
 			font-weight: bold;
-			border-bottom: 1px solid rgba(77, 201, 245, 0.3);
+			border-bottom: 2px solid rgba(77, 201, 245, 0.45);
 		}
-		.alert-head > div:nth-child(1) { width: 70px; }
+		.alert-head > div:nth-child(1) { width: 72px; }
 		.alert-head > div:nth-child(2) { flex: 1; }
 		.alert-row {
 			display: flex;
 			align-items: center;
-			height: 28px;
-			padding: 0 22px;
+			height: 32px;
+			padding: 0 20px;
 			color: #ffffff;
-			font-size: 13px;
+			font-size: 15px;
+			border-bottom: 1px solid rgba(77, 201, 245, 0.12);
 		}
-		.alert-row > div:nth-child(1) { width: 70px; color: #4DC9F5; font-weight: bold; }
-		.alert-row > div:nth-child(2) { flex: 1; }
+		.alert-row > div:nth-child(1) { width: 72px; color: #4DC9F5; font-weight: bold; font-size: 16px; }
+		.alert-row > div:nth-child(2) { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
 
-		/* ===== OP 表格 ===== */
+		/* ===== OP 表格(5 行加高、字号加大) ===== */
 		.op-table {
 			width: 100%;
+			height: 100%;
 			border-collapse: collapse;
 			color: #ffffff;
-			font-size: 16px;
+			font-size: 20px;
 			table-layout: fixed;
 		}
 		.op-table thead th {
-			height: 46px;
+			height: 58px;
 			color: #4DC9F5;
-			font-weight: normal;
-			font-size: 16px;
-			background: rgba(0, 100, 180, 0.25);
-			border-bottom: 1px solid rgba(77, 201, 245, 0.35);
+			font-weight: bold;
+			font-size: 20px;
+			letter-spacing: 1px;
+			background: rgba(0, 100, 180, 0.28);
+			border-bottom: 2px solid rgba(77, 201, 245, 0.5);
 			text-align: center;
 		}
 		.op-table tbody td {
-			height: 46px;
+			height: 88px;
 			text-align: center;
-			border-bottom: 1px dashed rgba(77, 201, 245, 0.18);
+			border-bottom: 1px solid rgba(77, 201, 245, 0.22);
+			vertical-align: middle;
+		}
+		.op-table tbody tr:last-child td {
+			border-bottom: 2px solid rgba(77, 201, 245, 0.35);
 		}
 		.op-table tbody tr:hover td { background: rgba(0, 200, 255, 0.06); }
-		.op-table td.op-col  { color: #00C8FF; font-weight: bold; font-size: 18px; }
-		.op-table td.op-name { color: #ffffff; text-align: center; }
-		.op-table td.op-val  { color: #00F7FF; font-weight: bold; font-size: 18px; }
+		.op-table td.op-col  { color: #00C8FF; font-weight: bold; font-size: 26px; }
+		.op-table td.op-name { color: #ffffff; text-align: center; font-size: 22px; }
+		.op-table td.op-val  { color: #00F7FF; font-weight: bold; font-size: 28px; }
 
-		/* ===== 产线阶段数据总览 (按模板排布) ===== */
+		/* ===== 产线阶段数据总览(对齐原 UI 四宫格框) ===== */
 		.overview-wrap {
 			position: relative;
 			height: 100%;
@@ -211,11 +215,6 @@
 			font-weight: bold;
 			line-height: 1;
 		}
-		.overview-card .oc-value .arrow {
-			font-size: 13px;
-			color: #00FF88;
-			margin-left: 3px;
-		}
 		.overview-gauge-wrap {
 			position: absolute;
 			top: 50%;
@@ -250,17 +249,17 @@
 			display: flex;
 			align-items: center;
 			justify-content: flex-end;
-			padding: 4px 18px 0;
-			font-size: 13px;
+			padding: 6px 22px 0;
+			font-size: 15px;
 			color: #ffffff;
-			gap: 18px;
+			gap: 22px;
 		}
 		.legend-inline .dot {
 			display: inline-block;
 			width: 12px;
 			height: 12px;
 			border-radius: 50%;
-			margin-right: 5px;
+			margin-right: 6px;
 			vertical-align: middle;
 		}
 	</style>
@@ -279,7 +278,7 @@
 
 	<div class="mes4-panel p-alert">
 		<div class="mes4-ptitle">  产线阶段报警信息  </div>
-		<div class="mes4-pbody" style="padding:0">
+		<div class="mes4-pbody">
 			<div class="alert-head"><div>序号</div><div>报警类型</div></div>
 			<div id="alertList"></div>
 		</div>
@@ -291,7 +290,7 @@
 			<span><i class="dot" style="background:#00FF88"></i>产出</span>
 			<span><i class="dot" style="background:#00C8FF"></i>不良</span>
 		</div>
-		<div class="mes4-pbody" style="top:62px"><canvas id="chartQuality" class="chart"></canvas></div>
+		<div class="mes4-pbody"><canvas id="chartQuality" class="chart"></canvas></div>
 	</div>
 
 	<div class="mes4-panel p-product">
@@ -300,16 +299,14 @@
 	</div>
 
 	<div class="mes4-panel p-table">
-		<div class="mes4-pbody" style="top:5px;padding:0 20px">
+		<div class="mes4-pbody">
 			<table class="op-table">
 				<thead>
 				<tr>
-					<th style="width:110px">工位</th>
-					<th style="width:200px">工位名称</th>
-					<th>计划产能</th>
-					<th>小时产能</th>
+					<th style="width:160px">工位</th>
+					<th style="width:300px">工位名称</th>
 					<th>实际产能</th>
-					<th>当班产能(12小时内)</th>
+					<th>当班产能</th>
 				</tr>
 				</thead>
 				<tbody id="opTableBody"></tbody>
@@ -329,26 +326,19 @@
 				<div class="overview-cards">
 					<div class="overview-card">
 						<div class="oc-label">生产人数</div>
-						<div class="oc-value"><span id="productionPeople">48</span> <span class="arrow">↑</span></div>
+						<div class="oc-value"><span id="productionPeople">48</span></div>
 					</div>
 					<div class="overview-card">
 						<div class="oc-label">设备数量</div>
-						<div class="oc-value"><span id="deviceCount">56</span> <span class="arrow">↑</span></div>
+						<div class="oc-value"><span id="deviceCount">56</span></div>
 					</div>
 					<div class="overview-card">
 						<div class="oc-label">计划产量</div>
-						<div class="oc-value"><span id="planProduction">221</span> <span class="arrow">↑</span></div>
+						<div class="oc-value"><span id="planProduction">221</span></div>
 					</div>
 					<div class="overview-card">
 						<div class="oc-label">实际产量</div>
-						<div class="oc-value"><span id="actualProduction">201</span> <span class="arrow">↑</span></div>
-					</div>
-				</div>
-				<div class="overview-gauge-wrap">
-					<canvas id="gaugeRate"></canvas>
-					<div class="gauge-txt">
-						<div class="val"><span id="gaugeVal">70</span>%</div>
-						<div class="lbl">今日达成率</div>
+						<div class="oc-value"><span id="actualProduction">201</span></div>
 					</div>
 				</div>
 			</div>
@@ -370,7 +360,7 @@
 			<span><i class="dot" style="background:#00FF88"></i>上线</span>
 			<span><i class="dot" style="background:#00C8FF"></i>下线</span>
 		</div>
-		<div class="mes4-pbody" style="top:62px"><canvas id="chartHourly" class="chart"></canvas></div>
+		<div class="mes4-pbody"><canvas id="chartHourly" class="chart"></canvas></div>
 	</div>
 </div>
 
@@ -390,10 +380,12 @@
 		document.getElementById('mes4Time').textContent = pad(h) + ':' + pad(mi) + ':' + pad(s);
 	}
 
-	// ====== 测试数据 ======
+	// ====== 页面数据 ======
 	var mockData = {};
-	/** 每小时上下线:mesProduct/hourlyUpDownOkData(本页 lineSn=MP,上线 OP120 / 下线 OP190) */
-	var HOURLY_API_CFG = { lineSn: 'MP', onlineOprno: 'OP120', offlineOprno: 'OP190' };
+	/** 本屏产线:XT(工位 OP060–OP100,与 mesLineProcess 一致) */
+	var LINE_SN = 'XT';
+	/** 每小时上下线:上线 OP060 / 下线 OP100 */
+	var HOURLY_API_CFG = { lineSn: LINE_SN, onlineOprno: 'OP060', offlineOprno: 'OP100' };
 	function buildHourLabels24() {
 		var hourLabels = [];
 		for (var i = 1; i <= 24; i++) {
@@ -438,24 +430,13 @@
 			weekLabels.push((d.getMonth() + 1) + '月' + d.getDate() + '日');
 		}
 
-		// 报警信息由 updateDeviceAlarmAlertsByApi 拉取 mesDeviceAlarm/listData,展示 type
+		// 报警信息由 updateDeviceAlarmAlertsByApi 拉取
 		mockData.alerts = [];
 
-		// OP 表格
-		mockData.opTable = [
-			{ code: 'OP10', name: '上料+打码',       plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP20', name: '底板组件-正面FSW', plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP25', name: '底板组件-打磨',    plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP30', name: '底板组件-反面FSW', plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP40', name: '底板组件-CNC',     plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP50', name: '干冰清洁',         plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP55', name: '模组梁+封板预装',  plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP60', name: '封板正面FSW',      plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP65', name: '打磨',            plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP70', name: '封板反面FSW',      plan: 50, hour: 50, real: 50, shift: 50 }
-		];
+		// 工位表由 mesLineProcess/listData(XT,OP060–OP100)拉取,不使用假数据
+		mockData.opTable = [];
 
-		// 数据卡
+		// 数据卡(总览固定展示用)
 		mockData.cards = {
 			productionPeople: 48,
 			deviceCount: 56,
@@ -521,15 +502,15 @@
 	}
 	function drawYAxis(ctx, padL, padT, cw, ch, maxV, minV) {
 		minV = minV || 0;
-		ctx.fillStyle = 'rgba(77,201,245,0.8)';
-		ctx.font = '11px Microsoft YaHei';
+		ctx.fillStyle = 'rgba(77,201,245,0.9)';
+		ctx.font = '13px Microsoft YaHei';
 		ctx.textAlign = 'right';
-		ctx.strokeStyle = 'rgba(0,143,253,0.18)';
+		ctx.strokeStyle = 'rgba(0,143,253,0.22)';
 		ctx.lineWidth = 1;
 		for (var i = 0; i <= 4; i++) {
 			var val = Math.round(minV + (maxV - minV) * (4 - i) / 4);
 			var y = padT + ch * i / 4;
-			ctx.fillText(val, padL - 5, y + 4);
+			ctx.fillText(val, padL - 6, y + 4);
 			ctx.beginPath();
 			ctx.moveTo(padL, y);
 			ctx.lineTo(padL + cw, y);
@@ -541,12 +522,12 @@
 	function drawDoubleBar(id, a, b, labels, maxV) {
 		var p = prepareCanvas(id); if (!p) return;
 		var ctx = p.ctx, w = p.w, h = p.h;
-		var padL = 38, padR = 10, padT = 12, padB = 22;
+		var padL = 46, padR = 12, padT = 16, padB = 28;
 		var cw = w - padL - padR, ch = h - padT - padB;
 		ctx.clearRect(0, 0, w, h);
 		drawYAxis(ctx, padL, padT, cw, ch, maxV);
 		if (!a || a.length < 1) return;
-		var gw = cw / a.length, bw = gw * 0.3;
+		var gw = cw / a.length, bw = gw * 0.28;
 		for (var i = 0; i < a.length; i++) {
 			var cx = padL + i * gw + gw / 2;
 			var h1 = (a[i] / maxV) * ch;
@@ -559,11 +540,11 @@
 			g2.addColorStop(0, '#00C8FF'); g2.addColorStop(1, 'rgba(0,200,255,0.15)');
 			ctx.fillStyle = g2;
 			ctx.fillRect(cx + 2, padT + ch - h2, bw, h2);
-			ctx.fillStyle = '#00FF88'; ctx.font = 'bold 10px Microsoft YaHei';
+			ctx.fillStyle = '#00FF88'; ctx.font = 'bold 12px Microsoft YaHei';
 			ctx.textAlign = 'center'; ctx.fillText(a[i], cx - bw / 2 - 2, padT + ch - h1 - 4);
 			ctx.fillStyle = '#00C8FF'; ctx.fillText(b[i], cx + bw / 2 + 2, padT + ch - h2 - 4);
-			ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = '11px Microsoft YaHei';
-			ctx.fillText(labels[i], cx, h - 6);
+			ctx.fillStyle = 'rgba(77,201,245,0.95)'; ctx.font = '13px Microsoft YaHei';
+			ctx.fillText(labels[i], cx, h - 8);
 		}
 	}
 
@@ -699,7 +680,7 @@
 	function drawAreaLine(id, up, down, labels, maxV) {
 		var p = prepareCanvas(id); if (!p) return;
 		var ctx = p.ctx, w = p.w, h = p.h;
-		var padL = 30, padR = 12, padT = 12, padB = 22;
+		var padL = 42, padR = 14, padT = 16, padB = 28;
 		var cw = w - padL - padR, ch = h - padT - padB;
 		ctx.clearRect(0, 0, w, h);
 		drawYAxis(ctx, padL, padT, cw, ch, maxV);
@@ -735,15 +716,15 @@
 					ctx.bezierCurveTo(cpx2, py2, cpx2, y2, x2, y2);
 				}
 			}
-			ctx.strokeStyle = color; ctx.lineWidth = 1.6; ctx.stroke();
+			ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke();
 		}
 		draw(up, '#00FF88');
 		draw(down, '#00C8FF');
-		ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = '10px Microsoft YaHei'; ctx.textAlign = 'center';
+		ctx.fillStyle = 'rgba(77,201,245,0.95)'; ctx.font = '12px Microsoft YaHei'; ctx.textAlign = 'center';
 		for (var i = 0; i < labels.length; i++) {
 			if (i % 2 === 0) {
 				var x = padL + i * stepX;
-				ctx.fillText(labels[i], x, h - 6);
+				ctx.fillText(labels[i], x, h - 8);
 			}
 		}
 	}
@@ -833,8 +814,6 @@
 			html += '<tr>';
 			html += '<td class="op-col">' + r.code + '</td>';
 			html += '<td class="op-name">' + r.name + '</td>';
-			html += '<td class="op-val">' + r.plan + '</td>';
-			html += '<td class="op-val">' + r.hour + '</td>';
 			html += '<td class="op-val">' + r.real + '</td>';
 			html += '<td class="op-val">' + r.shift + '</td>';
 			html += '</tr>';
@@ -843,42 +822,24 @@
 		if (el) el.innerHTML = html;
 	}
 
-	function applyLatestPlanNumsToTable(planNums) {
-		var list = mockData.opTable || [];
-		for (var i = 0; i < list.length; i++) {
-			list[i].plan = planNums;
-		}
-		renderTable();
+	function parseOprnoNum(code) {
+		var m = String(code || '').toUpperCase().match(/^OP(\d+)/);
+		return m ? parseInt(m[1], 10) : -1;
 	}
 
+	function isOprnoInRange(code, fromNum, toNum) {
+		var n = parseOprnoNum(code);
+		return n >= fromNum && n <= toNum;
+	}
+
+	function applyLatestPlanNumsToTable(planNums) {}
 	function calcRatePercent(actual, plan) {
 		actual = Number(actual) || 0;
 		plan = Number(plan) || 0;
-		if (plan <= 0) {
-			return 0;
-		}
+		if (plan <= 0) return 0;
 		return Math.round(actual * 100 / plan);
 	}
-
-	function syncAchieveRateWithPlan() {
-		mockData.cards = mockData.cards || {};
-		mockData.achieveRate = mockData.achieveRate || { labels: [], actual: [] };
-		var planNums = Number(mockData.cards.planProduction) || 0;
-		var actualProduction = Number(mockData.cards.actualProduction) || 0;
-		var labels = mockData.achieveRate.labels || [];
-		var actualSeries = mockData.achieveRate.actual || [];
-		var targetSeries = [];
-		for (var i = 0; i < labels.length; i++) {
-			targetSeries.push(planNums);
-		}
-		var maxVal = Math.max.apply(null, targetSeries.concat(actualSeries).concat([10]));
-		mockData.achieveRate.target = targetSeries;
-		mockData.achieveRate.max = maxVal <= 0 ? 10 : Math.ceil(maxVal * 1.1);
-		mockData.achieveRate.min = 0;
-		mockData.rate = calcRatePercent(actualProduction, planNums);
-		renderCards();
-		drawAllCharts();
-	}
+	function syncAchieveRateWithPlan() {}
 
 	function updateLineProcessTableByApi() {
 		$.ajax({
@@ -888,7 +849,7 @@
 			data: {
 				pageNo: 1,
 				pageSize: 5000,
-				lineSn: 'MP'
+				lineSn: LINE_SN
 			},
 			success: function(resp) {
 				var rows = extractUserList(resp);
@@ -896,19 +857,21 @@
 				for (var i = 0; i < rows.length; i++) {
 					var p = rows[i] || {};
 					var ls = String(p.lineSn || p.line_sn || '').toUpperCase();
-					if (ls !== 'MP') continue;
+					if (ls !== LINE_SN) continue;
+					var code = p.oprno || '';
+					if (!isOprnoInRange(code, 60, 100)) continue;
 					out.push({
-						code: p.oprno || '',
+						code: code,
 						name: p.title || '',
-						plan: (mockData.cards && mockData.cards.planProduction) || 0,
-						hour: 0,
 						real: 0,
-						shift: 0
+						shift: 0,
+						_opNum: parseOprnoNum(code)
 					});
 				}
+				out.sort(function(a, b) { return a._opNum - b._opNum; });
+				mockData.opTable = out;
+				renderTable();
 				if (out.length > 0) {
-					mockData.opTable = out;
-					renderTable();
 					updateHourlyCapacityByApi();
 				}
 			}
@@ -917,77 +880,56 @@
 
 	function updateHourlyCapacityByApi() {
 		var list = mockData.opTable || [];
-		if (!list.length) {
-			return;
-		}
+		if (!list.length) return;
 		var oprnos = [];
 		for (var i = 0; i < list.length; i++) {
-			if (list[i] && list[i].code) {
-				oprnos.push(list[i].code);
-			}
-		}
-		if (!oprnos.length) {
-			return;
+			if (list[i] && list[i].code) oprnos.push(list[i].code);
 		}
-		fetchCapacityInto('hourlyCapacityData', 'MP', oprnos, 'hour');
-		fetchCapacityInto('dayCapacityData', 'MP', oprnos, 'real');
-		fetchCapacityInto('shiftCapacityData', 'MP', oprnos, 'shift');
+		if (!oprnos.length) return;
+		fetchCapacityInto('dayCapacityData', LINE_SN, oprnos, 'real', {});
+		fetchCapacityInto('shiftCapacityData', LINE_SN, oprnos, 'shift', { fixedTwoShift: 'true' });
 	}
 
-	// 通用:拉取产能数据并写入表格指定字段(hour=小时产能, real=实际/当天产能, shift=当班产能)
-	function fetchCapacityInto(action, lineSn, oprnos, field) {
+	function fetchCapacityInto(action, lineSn, oprnos, field, extraData) {
+		var data = { lineSn: lineSn, oprnos: oprnos.join(',') };
+		if (extraData) {
+			for (var k in extraData) {
+				if (extraData.hasOwnProperty(k)) data[k] = extraData[k];
+			}
+		}
 		$.ajax({
 			url: '${ctx}/mes/mesProductRecord/' + action,
 			type: 'POST',
 			dataType: 'json',
-			data: {
-				lineSn: lineSn,
-				oprnos: oprnos.join(',')
-			},
+			data: data,
 			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
+				if (!resp || resp.result !== 'true' || !resp.data) return;
 				var map = resp.data || {};
 				var list = mockData.opTable || [];
 				for (var i = 0; i < list.length; i++) {
 					var code = list[i].code || '';
-					list[i][field] = Number(map[code]) || 0;
+					var formatted = code.length === 6 ? code.substring(0, 5) : code;
+					list[i][field] = Number(map[code] != null ? map[code] : map[formatted]) || 0;
 				}
 				renderTable();
 			}
 		});
 	}
 
-	function updateLatestPlanProductionByApi() {
-		$.ajax({
-			url: '${ctx}/mes/mesPlan/latestPlanNums',
-			type: 'POST',
-			dataType: 'json',
-			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
-				var planNums = Number(resp.data.nums) || 0;
-				mockData.cards = mockData.cards || {};
-				mockData.cards.planProduction = planNums;
-				applyLatestPlanNumsToTable(planNums);
-				syncAchieveRateWithPlan();
-			}
-		});
-	}
-
-	// 近七日产品质检数据分析(产出=半成品入库,不良=返工记录),按产线 MP 统计
+	// 近七日产品质检:产出=OP100 OK,不良=返工 OP070–OP100
 	function updateWeekQualityByApi() {
 		$.ajax({
 			url: '${ctx}/mes/mesProductRecord/weekQualityData',
 			type: 'POST',
 			dataType: 'json',
-			data: { lineSn: 'MP' },
+			data: {
+				lineSn: LINE_SN,
+				producedOprno: 'OP100',
+				defectOprnoFrom: 'OP070',
+				defectOprnoTo: 'OP100'
+			},
 			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
+				if (!resp || resp.result !== 'true' || !resp.data) return;
 				var d = resp.data;
 				var produced = d.produced || [];
 				var defect = d.defect || [];
@@ -1002,68 +944,16 @@
 					labels: d.labels || [],
 					produced: produced,
 					defect: defect,
-					max: Math.ceil(maxV * 1.2)
-				};
-				// 近七日产量分析(折线图):产量取每日入库数量(半成品入库管理)
-				var pMax = 0, pMin = Number.MAX_VALUE;
-				for (var k = 0; k < produced.length; k++) {
-					var v = Number(produced[k]) || 0;
-					if (v > pMax) pMax = v;
-					if (v < pMin) pMin = v;
-				}
-				if (pMin === Number.MAX_VALUE) { pMin = 0; }
-				var pad = Math.max(1, Math.round((pMax - pMin) * 0.2));
-				mockData.productTrend = {
-					labels: d.labels || [],
-					data: produced,
-					max: pMax + pad,
-					min: Math.max(0, pMin - pad)
+					max: Math.ceil(maxV * 1.15) || 10
 				};
 				drawAllCharts();
 			}
 		});
 	}
 
-	// 各月产量统计图(半成品入库管理按月统计),按产线 MP
-	function updateMonthlyProductByApi() {
-		$.ajax({
-			url: '${ctx}/mes/mesProductRecord/monthlyProductData',
-			type: 'POST',
-			dataType: 'json',
-			data: { lineSn: 'MP' },
-			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
-				var d = resp.data;
-				var data = d.data || [];
-				var maxV = 10;
-				for (var i = 0; i < data.length; i++) {
-					if (Number(data[i]) > maxV) maxV = Number(data[i]);
-				}
-				mockData.monthly = {
-					labels: d.labels || [],
-					data: data,
-					max: Math.ceil(maxV * 1.2)
-				};
-				drawAllCharts();
-			}
-		});
-	}
-	function renderCards() {
-		var c = mockData.cards || {};
-		var m = {
-			productionPeople: c.productionPeople,
-			deviceCount: c.deviceCount,
-			planProduction: c.planProduction,
-			actualProduction: c.actualProduction,
-			gaugeVal: mockData.rate
-		};
-		for (var k in m) {
-			var el = document.getElementById(k);
-			if (el && m[k] != null) el.textContent = m[k];
-		}
-	}
+	function updateMonthlyProductByApi() {}
+
+	function renderCards() {}
 
 	function extractUserList(resp) {
 		if (!resp) return [];
@@ -1105,60 +995,25 @@
 		});
 	}
 
-	function updateProductionPeopleByApi() {
+	/** 总览三卡:生产人数 / 设备数量 / 计划产量 ← mesProductScreen 最新配置 */
+	function updateScreenConfigByApi() {
 		$.ajax({
-			url: '${ctx}/sys/empUser/listData',
+			url: '${ctx}/mes/mesProductScreen/latestConfig',
 			type: 'POST',
 			dataType: 'json',
-			data: {
-				pageNo: 1,
-				pageSize: 5000,
-				'employee.office.officeName': '生产部'
-			},
 			success: function(resp) {
-				var peopleCount = 0;
-				var users = extractUserList(resp);
-				for (var i = 0; i < users.length; i++) {
-					var u = users[i] || {};
-					var office = (u.employee && u.employee.office) ? u.employee.office : {};
-					var officeName = office.officeName || '';
-					var normalizedOfficeName = (officeName + '').replace(/\s/g, '');
-					if (
-						normalizedOfficeName === '生产部' ||
-						/[/\\>]+生产部$/.test(normalizedOfficeName)
-					) {
-						peopleCount++;
-					}
-				}
-				mockData.cards = mockData.cards || {};
-				mockData.cards.productionPeople = peopleCount;
-				var target = document.getElementById('productionPeople');
-				if (target) target.textContent = peopleCount;
-			}
-		});
-	}
-
-	function updateDeviceCountByApi() {
-		$.ajax({
-			url: '${ctx}/mes/mesDevice/listData',
-			type: 'POST',
-			dataType: 'json',
-			data: {
-				pageNo: 1,
-				pageSize: 1
-			},
-			success: function(resp) {
-				var deviceCount = 0;
-				if (resp && typeof resp.count === 'number') {
-					deviceCount = resp.count;
-				} else {
-					var devices = extractUserList(resp);
-					deviceCount = devices.length;
-				}
+				if (!resp || resp.result !== 'true' || !resp.data) return;
+				var d = resp.data;
 				mockData.cards = mockData.cards || {};
-				mockData.cards.deviceCount = deviceCount;
-				var target = document.getElementById('deviceCount');
-				if (target) target.textContent = deviceCount;
+				mockData.cards.productionPeople = Number(d.peopleNums) || 0;
+				mockData.cards.deviceCount = Number(d.deviceNums) || 0;
+				mockData.cards.planProduction = Number(d.planNums) || 0;
+				var elPeople = document.getElementById('productionPeople');
+				var elDevice = document.getElementById('deviceCount');
+				var elPlan = document.getElementById('planProduction');
+				if (elPeople) elPeople.textContent = mockData.cards.productionPeople;
+				if (elDevice) elDevice.textContent = mockData.cards.deviceCount;
+				if (elPlan) elPlan.textContent = mockData.cards.planProduction;
 			}
 		});
 	}
@@ -1181,113 +1036,54 @@
 
 	function applyActualProductionCount(n) {
 		mockData.cards = mockData.cards || {};
-		mockData.cards.actualProduction = n;
-		syncAchieveRateWithPlan();
-	}
-
-	function getCalendarDayUpdateDateRange() {
-		var now = new Date();
-		var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
-		var dateStr = now.getFullYear() + '-' + pad(now.getMonth() + 1) + '-' + pad(now.getDate());
-		return {
-			gte: dateStr + ' 00:00:00',
-			lte: dateStr + ' 23:59:59',
-			startMs: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0).getTime(),
-			endMs: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime()
-		};
-	}
-
-	function parseRecordDateMs(val) {
-		if (val == null || val === '') {
-			return NaN;
-		}
-		if (typeof val === 'number' && !isNaN(val)) {
-			return val;
-		}
-		var s = String(val).trim().replace(/-/g, '/');
-		var t = Date.parse(s);
-		return isNaN(t) ? NaN : t;
-	}
-
-	function isRecordWithinToday(r, day) {
-		var t = parseRecordDateMs(r.updateDate || r.update_date);
-		if (isNaN(t)) {
-			return false;
-		}
-		return t >= day.startMs && t <= day.endMs;
+		mockData.cards.actualProduction = Number(n) || 0;
+		var el = document.getElementById('actualProduction');
+		if (el) el.textContent = mockData.cards.actualProduction;
 	}
 
+	/** 实际产量:取本阶段末工位当天 OK 数(半成品 OP100) */
 	function updateActualProductionByApi() {
-		var dayRange = getCalendarDayUpdateDateRange();
-		var pageSize = 3000;
-		var maxPages = 200;
-		var pageNo = 1;
-		var totalOk = 0;
-		function fetchPage() {
-			$.ajax({
-				url: '${ctx}/mes/mesProductRecord/listData',
-				type: 'POST',
-				dataType: 'json',
-				data: {
-					pageNo: pageNo,
-					pageSize: pageSize,
-					oprno: 'OP190',
-					content: 'OK',
-					lineSn: 'MP',
-					updateDate_gte: dayRange.gte,
-					updateDate_lte: dayRange.lte
-				},
-				success: function(resp) {
-					var list = extractUserList(resp);
-					for (var i = 0; i < list.length; i++) {
-						var r = list[i] || {};
-						if (isOpr190Family(r.oprno) && isRecordContentOk(r.content) && isRecordWithinToday(r, dayRange) && String(r.lineSn || r.line_sn || '').toUpperCase() === 'MP') {
-							totalOk++;
-						}
-					}
-					if (list.length === pageSize && pageNo < maxPages) {
-						pageNo++;
-						fetchPage();
-					} else {
-						applyActualProductionCount(totalOk);
-					}
-				},
-				error: function() {
-					applyActualProductionCount(totalOk);
+		var oprno = HOURLY_API_CFG.offlineOprno || 'OP100';
+		$.ajax({
+			url: '${ctx}/mes/mesProductRecord/dayCapacityData',
+			type: 'POST',
+			dataType: 'json',
+			data: {
+				lineSn: LINE_SN,
+				oprnos: oprno
+			},
+			success: function(resp) {
+				if (!resp || resp.result !== 'true' || !resp.data) {
+					applyActualProductionCount(0);
+					return;
 				}
-			});
-		}
-		fetchPage();
+				var map = resp.data || {};
+				var formatted = oprno.length === 6 ? oprno.substring(0, 5) : oprno;
+				applyActualProductionCount(Number(map[oprno] != null ? map[oprno] : map[formatted]) || 0);
+			},
+			error: function() {
+				applyActualProductionCount(0);
+			}
+		});
 	}
 
 	function drawAllCharts() {
 		var q = mockData.qualityInspect;
 		if (q) drawDoubleBar('chartQuality', q.produced, q.defect, q.labels, q.max);
-		var t = mockData.productTrend;
-		if (t) drawLineChart('chartProduct', t.data, t.labels, t.max, t.min, '#00C8FF');
-		var a = mockData.achieveRate;
-		if (a) drawDualLine('chartAchieve', a.target, a.actual, a.labels, a.max, a.min);
 		var hr = mockData.hourly;
 		if (hr) drawAreaLine('chartHourly', hr.up, hr.down, hr.labels, hr.max);
-		var mo = mockData.monthly;
-		if (mo) drawMonthBar('chartMonth', mo.data, mo.labels, mo.max);
-		drawGauge('gaugeRate', mockData.rate || 0);
 	}
 
 	function refreshData() {
 		generateMockData();
 		renderAlerts();
 		renderTable();
-		renderCards();
-		updateLatestPlanProductionByApi();
-		updateLineProcessTableByApi();
-		updateProductionPeopleByApi();
-		updateDeviceCountByApi();
+		updateScreenConfigByApi();
 		updateActualProductionByApi();
+		updateLineProcessTableByApi();
 		updateDeviceAlarmAlertsByApi();
 		updateHourlyUpDownByApi();
 		updateWeekQualityByApi();
-		updateMonthlyProductByApi();
 		drawAllCharts();
 	}
 

+ 156 - 367
src/main/resources/views/modules/mes/mesScreen5.html

@@ -99,23 +99,22 @@
 		}
 		.chart { width: 100%; height: 100%; display: block; }
 
-		/* ===== 位置定义 (1920 × 1080 画布) ===== */
-		/* 左列: x=30, 宽=430 */
-		.p-alert    { left: 40px;  top: 130px; width: 430px; height: 290px; }
-		.p-quality  { left: 40px;  top: 455px; width: 430px; height: 265px; }
-		.p-product  { left: 40px;  top: 763px; width: 430px; height: 306px; }
+		/* ===== 位置:与 screen3 高度规则一致 ===== */
+		.p-alert    { left: 36px;  top: 118px; width: 440px; height: 430px; }
+		.p-quality  { left: 36px;  top: 568px; width: 440px; height: 460px; }
+		.p-product  { display: none; }
 
-		/* 中列: x=480, 宽=960 */
-		.p-table    { left: 480px; top: 130px; width: 960px; height: 575px; }
-		.p-month    { left: 480px; top: 725px; width: 960px; height: 306px; }
+		.p-table    { left: 496px; top: 118px; width: 928px; height: 560px; }
+		.p-month    { display: none; }
 
-		/* 右列: right=30, 宽=430 */
 		.p-overview { right: 30px; top: 130px; width: 430px; height: 290px; }
-		.p-achieve  { right: 40px; top: 455px; width: 430px; height: 265px; }
-		.p-hourly   { right: 40px; top: 763px; width: 430px; height: 306px; }
+		.p-achieve  { display: none; }
+		.p-hourly   { right: 36px; top: 568px; width: 440px; height: 460px; }
 
-		/* 阶段报警信息 - 按模板比例调整 */
-		.p-alert .mes4-ptitle {
+		.p-alert .mes4-ptitle,
+		.p-overview .mes4-ptitle,
+		.p-quality .mes4-ptitle,
+		.p-hourly .mes4-ptitle {
 			height: 40px;
 			line-height: 40px;
 			font-size: 16px;
@@ -123,10 +122,11 @@
 			font-weight: bold;
 			letter-spacing: 1px;
 		}
-		.p-alert .mes4-pbody  { top: 40px; padding: 0; }
-		/* 总览标题 */
-		.p-overview .mes4-ptitle { height: 40px; line-height: 40px; font-size: 16px; }
-		.p-overview .mes4-pbody  { top: 40px; }
+		.p-alert .mes4-pbody { top: 40px; padding: 0 6px 8px; }
+		.p-overview .mes4-pbody { top: 40px; padding: 0; }
+		.p-table .mes4-pbody { top: 8px; padding: 8px 28px 12px; }
+		.p-quality .mes4-pbody,
+		.p-hourly .mes4-pbody { top: 68px; }
 
 		/* ===== 报警信息 ===== */
 		.alert-head {
@@ -152,27 +152,33 @@
 		.alert-row > div:nth-child(1) { width: 70px; color: #4DC9F5; font-weight: bold; }
 		.alert-row > div:nth-child(2) { flex: 1; }
 
-		/* ===== OP 表格 ===== */
+		/* ===== OP 表格(6 工位铺满,字号加大) ===== */
 		.op-table {
 			width: 100%;
+			height: calc(100% - 28px);
 			border-collapse: collapse;
 			color: #ffffff;
-			font-size: 16px;
+			font-size: 20px;
 			table-layout: fixed;
 		}
 		.op-table thead th {
-			height: 46px;
+			height: 56px;
 			color: #4DC9F5;
-			font-weight: normal;
-			font-size: 16px;
-			background: rgba(0, 100, 180, 0.25);
-			border-bottom: 1px solid rgba(77, 201, 245, 0.35);
+			font-weight: bold;
+			font-size: 20px;
+			letter-spacing: 1px;
+			background: rgba(0, 100, 180, 0.28);
+			border-bottom: 2px solid rgba(77, 201, 245, 0.5);
 			text-align: center;
 		}
 		.op-table tbody td {
-			height: 46px;
+			height: 76px;
 			text-align: center;
-			border-bottom: 1px dashed rgba(77, 201, 245, 0.18);
+			border-bottom: 1px solid rgba(77, 201, 245, 0.22);
+			vertical-align: middle;
+		}
+		.op-table tbody tr:last-child td {
+			border-bottom: 2px solid rgba(77, 201, 245, 0.35);
 		}
 		.op-table tbody tr:hover td { background: rgba(0, 200, 255, 0.06); }
 		.op-table tbody tr.op-row-alt td {
@@ -180,24 +186,24 @@
 		}
 		.op-table tbody tr.op-row-alt:hover td { background: rgba(0, 200, 255, 0.08); }
 		.op-table tbody tr.op-row-empty td {
-			height: 46px;
+			height: 76px;
 			color: transparent;
-			border-bottom: 1px dashed rgba(77, 201, 245, 0.1);
+			border-bottom: 1px solid rgba(77, 201, 245, 0.12);
 			user-select: none;
 		}
 		.op-table-page-hint {
 			text-align: right;
-			height: 24px;
-			line-height: 24px;
-			font-size: 14px;
+			height: 26px;
+			line-height: 26px;
+			font-size: 15px;
 			color: #4DC9F5;
 			padding-right: 4px;
 			margin-bottom: 2px;
 			letter-spacing: 1px;
 		}
-		.op-table td.op-col  { color: #00C8FF; font-weight: bold; font-size: 18px; }
-		.op-table td.op-name { color: #ffffff; text-align: center; }
-		.op-table td.op-val  { color: #00F7FF; font-weight: bold; font-size: 18px; }
+		.op-table td.op-col  { color: #00C8FF; font-weight: bold; font-size: 26px; }
+		.op-table td.op-name { color: #ffffff; text-align: center; font-size: 22px; }
+		.op-table td.op-val  { color: #00F7FF; font-weight: bold; font-size: 28px; }
 
 		/* ===== 产线阶段数据总览 (按模板排布) ===== */
 		.overview-wrap {
@@ -231,18 +237,8 @@
 			font-weight: bold;
 			line-height: 1;
 		}
-		.overview-card .oc-value .arrow {
-			font-size: 13px;
-			color: #00FF88;
-			margin-left: 3px;
-		}
 		.overview-gauge-wrap {
-			position: absolute;
-			top: 50%;
-			left: 50%;
-			transform: translate(calc(-50% - 10px), calc(-50% + 16px));
-			width: 160px;
-			height: 160px;
+			display: none;
 		}
 		.overview-gauge-wrap canvas { width: 100%; height: 100%; }
 		.overview-gauge-wrap .gauge-txt {
@@ -299,7 +295,7 @@
 
 	<div class="mes4-panel p-alert">
 		<div class="mes4-ptitle">  产线阶段报警信息  </div>
-		<div class="mes4-pbody" style="padding:0">
+		<div class="mes4-pbody">
 			<div class="alert-head"><div>序号</div><div>报警类型</div></div>
 			<div id="alertList"></div>
 		</div>
@@ -311,7 +307,7 @@
 			<span><i class="dot" style="background:#00FF88"></i>产出</span>
 			<span><i class="dot" style="background:#00C8FF"></i>不良</span>
 		</div>
-		<div class="mes4-pbody" style="top:62px"><canvas id="chartQuality" class="chart"></canvas></div>
+		<div class="mes4-pbody"><canvas id="chartQuality" class="chart"></canvas></div>
 	</div>
 
 	<div class="mes4-panel p-product">
@@ -320,17 +316,15 @@
 	</div>
 
 	<div class="mes4-panel p-table">
-		<div class="mes4-pbody" style="top:5px;padding:0 20px">
+		<div class="mes4-pbody">
 			<div class="op-table-page-hint" id="opTablePageHint"></div>
 			<table class="op-table">
 				<thead>
 				<tr>
-					<th style="width:110px">工位</th>
-					<th style="width:200px">工位名称</th>
-					<th>计划产能</th>
-					<th>小时产能</th>
+					<th style="width:160px">工位</th>
+					<th style="width:300px">工位名称</th>
 					<th>实际产能</th>
-					<th>当班产能(12小时内)</th>
+					<th>当班产能</th>
 				</tr>
 				</thead>
 				<tbody id="opTableBody"></tbody>
@@ -350,26 +344,19 @@
 				<div class="overview-cards">
 					<div class="overview-card">
 						<div class="oc-label">生产人数</div>
-						<div class="oc-value"><span id="productionPeople">48</span> <span class="arrow">↑</span></div>
+						<div class="oc-value"><span id="productionPeople">48</span></div>
 					</div>
 					<div class="overview-card">
 						<div class="oc-label">设备数量</div>
-						<div class="oc-value"><span id="deviceCount">56</span> <span class="arrow">↑</span></div>
+						<div class="oc-value"><span id="deviceCount">56</span></div>
 					</div>
 					<div class="overview-card">
 						<div class="oc-label">计划产量</div>
-						<div class="oc-value"><span id="planProduction">221</span> <span class="arrow">↑</span></div>
+						<div class="oc-value"><span id="planProduction">221</span></div>
 					</div>
 					<div class="overview-card">
 						<div class="oc-label">实际产量</div>
-						<div class="oc-value"><span id="actualProduction">201</span> <span class="arrow">↑</span></div>
-					</div>
-				</div>
-				<div class="overview-gauge-wrap">
-					<canvas id="gaugeRate"></canvas>
-					<div class="gauge-txt">
-						<div class="val"><span id="gaugeVal">70</span>%</div>
-						<div class="lbl">今日达成率</div>
+						<div class="oc-value"><span id="actualProduction">201</span></div>
 					</div>
 				</div>
 			</div>
@@ -391,7 +378,7 @@
 			<span><i class="dot" style="background:#00FF88"></i>上线</span>
 			<span><i class="dot" style="background:#00C8FF"></i>下线</span>
 		</div>
-		<div class="mes4-pbody" style="top:62px"><canvas id="chartHourly" class="chart"></canvas></div>
+		<div class="mes4-pbody"><canvas id="chartHourly" class="chart"></canvas></div>
 	</div>
 </div>
 
@@ -401,7 +388,7 @@
 	function pad(n) { return n < 10 ? '0' + n : '' + n; }
 
 	/** 工位表:每页条数与自动翻页间隔(毫秒) */
-	var OP_TABLE_PAGE_SIZE = 10;
+	var OP_TABLE_PAGE_SIZE = 6;
 	var OP_TABLE_PAGE_ROTATE_MS = 8000;
 	var opTablePageIndex = 0;
 	var opTableRotateTimer = null;
@@ -409,7 +396,7 @@
 	function padOpTableRowsToPageSize(html, rowCount) {
 		for (var j = rowCount; j < OP_TABLE_PAGE_SIZE; j++) {
 			var alt = (j % 2 === 1) ? ' op-row-alt' : '';
-			html += '<tr class="op-row-empty' + alt + '"><td colspan="6">&nbsp;</td></tr>';
+			html += '<tr class="op-row-empty' + alt + '"><td colspan="4">&nbsp;</td></tr>';
 		}
 		return html;
 	}
@@ -457,10 +444,14 @@
 		document.getElementById('mes4Time').textContent = pad(h) + ':' + pad(mi) + ':' + pad(s);
 	}
 
-	// ====== 测试数据 ======
+	// ====== 页面数据 ======
 	var mockData = {};
-	/** 每小时上下线:mesProduct/hourlyUpDownOkData(本页 lineSn=XT,上线 OP010 / 下线 OP180) */
-	var HOURLY_API_CFG = { lineSn: 'XT', onlineOprno: 'OP010', offlineOprno: 'OP180' };
+	/** 本屏产线:XT(工位 OP110–OP160) */
+	var LINE_SN = 'XT';
+	var OP_FROM = 110;
+	var OP_TO = 160;
+	/** 每小时上下线:上线 OP110 / 下线 OP160 */
+	var HOURLY_API_CFG = { lineSn: LINE_SN, onlineOprno: 'OP110', offlineOprno: 'OP160' };
 	function buildHourLabels24() {
 		var labels = [];
 		for (var i = 1; i <= 24; i++) {
@@ -505,26 +496,13 @@
 			weekLabels.push((d.getMonth() + 1) + '月' + d.getDate() + '日');
 		}
 
-		// 报警信息由 updateDeviceAlarmAlertsByApi 拉取 mesDeviceAlarm/listData,展示 type
+		// 报警信息由 updateDeviceAlarmAlertsByApi 拉取
 		mockData.alerts = [];
 
-		// OP 表格
-		mockData.opTable = [
-			{ code: 'OP10', name: '上料+打码',       plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP20', name: '底板组件-正面FSW', plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP25', name: '底板组件-打磨',    plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP30', name: '底板组件-反面FSW', plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP40', name: '底板组件-CNC',     plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP50', name: '干冰清洁',         plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP55', name: '模组梁+封板预装',  plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP60', name: '封板正面FSW',      plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP65', name: '打磨',            plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP70', name: '封板反面FSW',      plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP75', name: '下线检测',         plan: 50, hour: 50, real: 50, shift: 50 },
-			{ code: 'OP80', name: '装箱',             plan: 50, hour: 50, real: 50, shift: 50 }
-		];
-
-		// 数据卡
+		// 工位表由 mesLineProcess(XT,OP110–OP160)拉取
+		mockData.opTable = [];
+
+		// 数据卡(总览固定展示)
 		mockData.cards = {
 			productionPeople: 48,
 			deviceCount: 56,
@@ -908,8 +886,6 @@
 			html += '<tr' + ((i % 2 === 1) ? ' class="op-row-alt"' : '') + '>';
 			html += '<td class="op-col">' + r.code + '</td>';
 			html += '<td class="op-name">' + r.name + '</td>';
-			html += '<td class="op-val">' + r.plan + '</td>';
-			html += '<td class="op-val">' + r.hour + '</td>';
 			html += '<td class="op-val">' + r.real + '</td>';
 			html += '<td class="op-val">' + r.shift + '</td>';
 			html += '</tr>';
@@ -929,41 +905,17 @@
 		}
 	}
 
-	function applyLatestPlanNumsToTable(planNums) {
-		var list = mockData.opTable || [];
-		for (var i = 0; i < list.length; i++) {
-			list[i].plan = planNums;
-		}
-		renderTable();
-	}
+	function applyLatestPlanNumsToTable(planNums) {}
+	function syncAchieveRateWithPlan() {}
 
-	function calcRatePercent(actual, plan) {
-		actual = Number(actual) || 0;
-		plan = Number(plan) || 0;
-		if (plan <= 0) {
-			return 0;
-		}
-		return Math.round(actual * 100 / plan);
+	function parseOprnoNum(code) {
+		var m = String(code || '').toUpperCase().match(/^OP(\d+)/);
+		return m ? parseInt(m[1], 10) : -1;
 	}
 
-	function syncAchieveRateWithPlan() {
-		mockData.cards = mockData.cards || {};
-		mockData.achieveRate = mockData.achieveRate || { labels: [], actual: [] };
-		var planNums = Number(mockData.cards.planProduction) || 0;
-		var actualProduction = Number(mockData.cards.actualProduction) || 0;
-		var labels = mockData.achieveRate.labels || [];
-		var actualSeries = mockData.achieveRate.actual || [];
-		var targetSeries = [];
-		for (var i = 0; i < labels.length; i++) {
-			targetSeries.push(planNums);
-		}
-		var maxVal = Math.max.apply(null, targetSeries.concat(actualSeries).concat([10]));
-		mockData.achieveRate.target = targetSeries;
-		mockData.achieveRate.max = maxVal <= 0 ? 10 : Math.ceil(maxVal * 1.1);
-		mockData.achieveRate.min = 0;
-		mockData.rate = calcRatePercent(actualProduction, planNums);
-		renderCards();
-		drawAllCharts();
+	function isOprnoInRange(code, fromNum, toNum) {
+		var n = parseOprnoNum(code);
+		return n >= fromNum && n <= toNum;
 	}
 
 	function updateLineProcessTableByApi() {
@@ -974,7 +926,7 @@
 			data: {
 				pageNo: 1,
 				pageSize: 5000,
-				lineSn: 'XT'
+				lineSn: LINE_SN
 			},
 			success: function(resp) {
 				var rows = extractUserList(resp);
@@ -982,21 +934,23 @@
 				for (var i = 0; i < rows.length; i++) {
 					var p = rows[i] || {};
 					var ls = String(p.lineSn || p.line_sn || '').toUpperCase();
-					if (ls !== 'XT') continue;
+					if (ls !== LINE_SN) continue;
+					var code = p.oprno || '';
+					if (!isOprnoInRange(code, OP_FROM, OP_TO)) continue;
 					out.push({
-						code: p.oprno || '',
+						code: code,
 						name: p.title || '',
-						plan: (mockData.cards && mockData.cards.planProduction) || 0,
-						hour: 0,
 						real: 0,
-						shift: 0
+						shift: 0,
+						_opNum: parseOprnoNum(code)
 					});
 				}
+				out.sort(function(a, b) { return a._opNum - b._opNum; });
+				mockData.opTable = out;
+				opTablePageIndex = 0;
+				renderTable();
+				startOpTablePageRotate();
 				if (out.length > 0) {
-					mockData.opTable = out;
-					opTablePageIndex = 0;
-					renderTable();
-					startOpTablePageRotate();
 					updateHourlyCapacityByApi();
 				}
 			}
@@ -1005,59 +959,56 @@
 
 	function updateHourlyCapacityByApi() {
 		var list = mockData.opTable || [];
-		if (!list.length) {
-			return;
-		}
+		if (!list.length) return;
 		var oprnos = [];
 		for (var i = 0; i < list.length; i++) {
-			if (list[i] && list[i].code) {
-				oprnos.push(list[i].code);
-			}
-		}
-		if (!oprnos.length) {
-			return;
+			if (list[i] && list[i].code) oprnos.push(list[i].code);
 		}
-		fetchCapacityInto('hourlyCapacityData', 'XT', oprnos, 'hour');
-		fetchCapacityInto('dayCapacityData', 'XT', oprnos, 'real');
-		fetchCapacityInto('shiftCapacityData', 'XT', oprnos, 'shift');
+		if (!oprnos.length) return;
+		fetchCapacityInto('dayCapacityData', LINE_SN, oprnos, 'real', {});
+		fetchCapacityInto('shiftCapacityData', LINE_SN, oprnos, 'shift', { fixedTwoShift: 'true' });
 	}
 
-	// 通用:拉取产能数据并写入表格指定字段(hour=小时产能, real=实际/当天产能, shift=当班产能)
-	function fetchCapacityInto(action, lineSn, oprnos, field) {
+	function fetchCapacityInto(action, lineSn, oprnos, field, extraData) {
+		var data = { lineSn: lineSn, oprnos: oprnos.join(',') };
+		if (extraData) {
+			for (var k in extraData) {
+				if (extraData.hasOwnProperty(k)) data[k] = extraData[k];
+			}
+		}
 		$.ajax({
 			url: '${ctx}/mes/mesProductRecord/' + action,
 			type: 'POST',
 			dataType: 'json',
-			data: {
-				lineSn: lineSn,
-				oprnos: oprnos.join(',')
-			},
+			data: data,
 			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
+				if (!resp || resp.result !== 'true' || !resp.data) return;
 				var map = resp.data || {};
 				var list = mockData.opTable || [];
 				for (var i = 0; i < list.length; i++) {
 					var code = list[i].code || '';
-					list[i][field] = Number(map[code]) || 0;
+					var formatted = code.length === 6 ? code.substring(0, 5) : code;
+					list[i][field] = Number(map[code] != null ? map[code] : map[formatted]) || 0;
 				}
 				renderTable();
 			}
 		});
 	}
 
-	// 近七日产品质检数据分析(产出=半成品入库,不良=返工记录),按产线 XT 统计
+	// 近七日质检:产出=OP160 OK,不良=返工 OP120–OP160
 	function updateWeekQualityByApi() {
 		$.ajax({
 			url: '${ctx}/mes/mesProductRecord/weekQualityData',
 			type: 'POST',
 			dataType: 'json',
-			data: { lineSn: 'XT' },
+			data: {
+				lineSn: LINE_SN,
+				producedOprno: 'OP160',
+				defectOprnoFrom: 'OP120',
+				defectOprnoTo: 'OP160'
+			},
 			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
+				if (!resp || resp.result !== 'true' || !resp.data) return;
 				var d = resp.data;
 				var produced = d.produced || [];
 				var defect = d.defect || [];
@@ -1072,86 +1023,18 @@
 					labels: d.labels || [],
 					produced: produced,
 					defect: defect,
-					max: Math.ceil(maxV * 1.2)
-				};
-				// 近七日产量分析(折线图):产量取每日入库数量(半成品入库管理)
-				var pMax = 0, pMin = Number.MAX_VALUE;
-				for (var k = 0; k < produced.length; k++) {
-					var v = Number(produced[k]) || 0;
-					if (v > pMax) pMax = v;
-					if (v < pMin) pMin = v;
-				}
-				if (pMin === Number.MAX_VALUE) { pMin = 0; }
-				var pad = Math.max(1, Math.round((pMax - pMin) * 0.2));
-				mockData.productTrend = {
-					labels: d.labels || [],
-					data: produced,
-					max: pMax + pad,
-					min: Math.max(0, pMin - pad)
+					max: Math.ceil(maxV * 1.15) || 10
 				};
 				drawAllCharts();
 			}
 		});
 	}
 
-	// 各月产量统计图(半成品入库管理按月统计),按产线 XT
-	function updateMonthlyProductByApi() {
-		$.ajax({
-			url: '${ctx}/mes/mesProductRecord/monthlyProductData',
-			type: 'POST',
-			dataType: 'json',
-			data: { lineSn: 'XT' },
-			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
-				var d = resp.data;
-				var data = d.data || [];
-				var maxV = 10;
-				for (var i = 0; i < data.length; i++) {
-					if (Number(data[i]) > maxV) maxV = Number(data[i]);
-				}
-				mockData.monthly = {
-					labels: d.labels || [],
-					data: data,
-					max: Math.ceil(maxV * 1.2)
-				};
-				drawAllCharts();
-			}
-		});
-	}
+	function updateMonthlyProductByApi() {}
 
-	function updateLatestPlanProductionByApi() {
-		$.ajax({
-			url: '${ctx}/mes/mesPlan/latestPlanNums',
-			type: 'POST',
-			dataType: 'json',
-			success: function(resp) {
-				if (!resp || resp.result !== 'true' || !resp.data) {
-					return;
-				}
-				var planNums = Number(resp.data.nums) || 0;
-				mockData.cards = mockData.cards || {};
-				mockData.cards.planProduction = planNums;
-				applyLatestPlanNumsToTable(planNums);
-				syncAchieveRateWithPlan();
-			}
-		});
-	}
-	function renderCards() {
-		var c = mockData.cards || {};
-		var m = {
-			productionPeople: c.productionPeople,
-			deviceCount: c.deviceCount,
-			planProduction: c.planProduction,
-			actualProduction: c.actualProduction,
-			gaugeVal: mockData.rate
-		};
-		for (var k in m) {
-			var el = document.getElementById(k);
-			if (el && m[k] != null) el.textContent = m[k];
-		}
-	}
+	function updateLatestPlanProductionByApi() {}
+
+	function renderCards() {}
 
 	function extractUserList(resp) {
 		if (!resp) return [];
@@ -1193,60 +1076,25 @@
 		});
 	}
 
-	function updateProductionPeopleByApi() {
-		$.ajax({
-			url: '${ctx}/sys/empUser/listData',
-			type: 'POST',
-			dataType: 'json',
-			data: {
-				pageNo: 1,
-				pageSize: 5000,
-				'employee.office.officeName': '生产部'
-			},
-			success: function(resp) {
-				var peopleCount = 0;
-				var users = extractUserList(resp);
-				for (var i = 0; i < users.length; i++) {
-					var u = users[i] || {};
-					var office = (u.employee && u.employee.office) ? u.employee.office : {};
-					var officeName = office.officeName || '';
-					var normalizedOfficeName = (officeName + '').replace(/\s/g, '');
-					if (
-						normalizedOfficeName === '生产部' ||
-						/[/\\>]+生产部$/.test(normalizedOfficeName)
-					) {
-						peopleCount++;
-					}
-				}
-				mockData.cards = mockData.cards || {};
-				mockData.cards.productionPeople = peopleCount;
-				var target = document.getElementById('productionPeople');
-				if (target) target.textContent = peopleCount;
-			}
-		});
-	}
-
-	function updateDeviceCountByApi() {
+	/** 总览三卡:生产人数 / 设备数量 / 计划产量 ← mesProductScreen 最新配置 */
+	function updateScreenConfigByApi() {
 		$.ajax({
-			url: '${ctx}/mes/mesDevice/listData',
+			url: '${ctx}/mes/mesProductScreen/latestConfig',
 			type: 'POST',
 			dataType: 'json',
-			data: {
-				pageNo: 1,
-				pageSize: 1
-			},
 			success: function(resp) {
-				var deviceCount = 0;
-				if (resp && typeof resp.count === 'number') {
-					deviceCount = resp.count;
-				} else {
-					var devices = extractUserList(resp);
-					deviceCount = devices.length;
-				}
+				if (!resp || resp.result !== 'true' || !resp.data) return;
+				var d = resp.data;
 				mockData.cards = mockData.cards || {};
-				mockData.cards.deviceCount = deviceCount;
-				var target = document.getElementById('deviceCount');
-				if (target) target.textContent = deviceCount;
+				mockData.cards.productionPeople = Number(d.peopleNums) || 0;
+				mockData.cards.deviceCount = Number(d.deviceNums) || 0;
+				mockData.cards.planProduction = Number(d.planNums) || 0;
+				var elPeople = document.getElementById('productionPeople');
+				var elDevice = document.getElementById('deviceCount');
+				var elPlan = document.getElementById('planProduction');
+				if (elPeople) elPeople.textContent = mockData.cards.productionPeople;
+				if (elDevice) elDevice.textContent = mockData.cards.deviceCount;
+				if (elPlan) elPlan.textContent = mockData.cards.planProduction;
 			}
 		});
 	}
@@ -1269,97 +1117,42 @@
 
 	function applyActualProductionCount(n) {
 		mockData.cards = mockData.cards || {};
-		mockData.cards.actualProduction = n;
-		syncAchieveRateWithPlan();
-	}
-
-	function getCalendarDayUpdateDateRange() {
-		var now = new Date();
-		var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
-		var dateStr = now.getFullYear() + '-' + pad(now.getMonth() + 1) + '-' + pad(now.getDate());
-		return {
-			gte: dateStr + ' 00:00:00',
-			lte: dateStr + ' 23:59:59',
-			startMs: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0).getTime(),
-			endMs: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime()
-		};
-	}
-
-	function parseRecordDateMs(val) {
-		if (val == null || val === '') {
-			return NaN;
-		}
-		if (typeof val === 'number' && !isNaN(val)) {
-			return val;
-		}
-		var s = String(val).trim().replace(/-/g, '/');
-		var t = Date.parse(s);
-		return isNaN(t) ? NaN : t;
-	}
-
-	function isRecordWithinToday(r, day) {
-		var t = parseRecordDateMs(r.updateDate || r.update_date);
-		if (isNaN(t)) {
-			return false;
-		}
-		return t >= day.startMs && t <= day.endMs;
+		mockData.cards.actualProduction = Number(n) || 0;
+		var el = document.getElementById('actualProduction');
+		if (el) el.textContent = mockData.cards.actualProduction;
 	}
 
+	/** 实际产量:取本阶段末工位当天 OK 数(机加 OP160) */
 	function updateActualProductionByApi() {
-		var dayRange = getCalendarDayUpdateDateRange();
-		var pageSize = 3000;
-		var maxPages = 200;
-		var pageNo = 1;
-		var totalOk = 0;
-		function fetchPage() {
-			$.ajax({
-				url: '${ctx}/mes/mesProductRecord/listData',
-				type: 'POST',
-				dataType: 'json',
-				data: {
-					pageNo: pageNo,
-					pageSize: pageSize,
-					oprno: 'OP180',
-					content: 'OK',
-					lineSn: 'XT',
-					updateDate_gte: dayRange.gte,
-					updateDate_lte: dayRange.lte
-				},
-				success: function(resp) {
-					var list = extractUserList(resp);
-					for (var i = 0; i < list.length; i++) {
-						var r = list[i] || {};
-						if (isOpr180Family(r.oprno) && isRecordContentOk(r.content) && isRecordWithinToday(r, dayRange) && String(r.lineSn || r.line_sn || '').toUpperCase() === 'XT') {
-							totalOk++;
-						}
-					}
-					if (list.length === pageSize && pageNo < maxPages) {
-						pageNo++;
-						fetchPage();
-					} else {
-						applyActualProductionCount(totalOk);
-					}
-				},
-				error: function() {
-					applyActualProductionCount(totalOk);
+		var oprno = HOURLY_API_CFG.offlineOprno || 'OP160';
+		$.ajax({
+			url: '${ctx}/mes/mesProductRecord/dayCapacityData',
+			type: 'POST',
+			dataType: 'json',
+			data: {
+				lineSn: LINE_SN,
+				oprnos: oprno
+			},
+			success: function(resp) {
+				if (!resp || resp.result !== 'true' || !resp.data) {
+					applyActualProductionCount(0);
+					return;
 				}
-			});
-		}
-		fetchPage();
+				var map = resp.data || {};
+				var formatted = oprno.length === 6 ? oprno.substring(0, 5) : oprno;
+				applyActualProductionCount(Number(map[oprno] != null ? map[oprno] : map[formatted]) || 0);
+			},
+			error: function() {
+				applyActualProductionCount(0);
+			}
+		});
 	}
 
 	function drawAllCharts() {
 		var q = mockData.qualityInspect;
 		if (q) drawDoubleBar('chartQuality', q.produced, q.defect, q.labels, q.max);
-		var t = mockData.productTrend;
-		if (t) drawLineChart('chartProduct', t.data, t.labels, t.max, t.min, '#00C8FF');
-		var a = mockData.achieveRate;
-		if (a) drawDualLine('chartAchieve', a.target, a.actual, a.labels, a.max, a.min);
 		var hr = mockData.hourly;
 		if (hr) drawAreaLine('chartHourly', hr.up, hr.down, hr.labels, hr.max);
-		var mo = mockData.monthly;
-		if (mo) drawMonthBar('chartMonth', mo.data, mo.labels, mo.max);
-		drawGauge('gaugeRate', mockData.rate || 0);
 	}
 
 	function refreshData() {
@@ -1368,16 +1161,12 @@
 		renderAlerts();
 		renderTable();
 		startOpTablePageRotate();
-		renderCards();
-		updateLatestPlanProductionByApi();
-		updateLineProcessTableByApi();
-		updateProductionPeopleByApi();
-		updateDeviceCountByApi();
+		updateScreenConfigByApi();
 		updateActualProductionByApi();
+		updateLineProcessTableByApi();
 		updateDeviceAlarmAlertsByApi();
 		updateHourlyUpDownByApi();
 		updateWeekQualityByApi();
-		updateMonthlyProductByApi();
 		drawAllCharts();
 	}