liuwei 2 месяцев назад
Родитель
Сommit
da7651a696
20 измененных файлов с 1294 добавлено и 180 удалено
  1. 15 0
      mescloud-ljr.iml
  2. 15 0
      src/main/java/com/jeesite/modules/mes/dao/MesProductTjDao.java
  3. 10 0
      src/main/java/com/jeesite/modules/mes/entity/MesDeviceAlarm.java
  4. 96 0
      src/main/java/com/jeesite/modules/mes/entity/MesProductTj.java
  5. 40 0
      src/main/java/com/jeesite/modules/mes/service/MesDeviceAlarmService.java
  6. 136 0
      src/main/java/com/jeesite/modules/mes/service/MesProductRecordService.java
  7. 81 0
      src/main/java/com/jeesite/modules/mes/service/MesProductTjService.java
  8. 41 14
      src/main/java/com/jeesite/modules/mes/service/MesShiftService.java
  9. 22 10
      src/main/java/com/jeesite/modules/mes/web/MesProductController.java
  10. 38 0
      src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java
  11. 5 2
      src/main/java/com/jeesite/modules/mes/web/MesProductScreenController.java
  12. 145 0
      src/main/java/com/jeesite/modules/mes/web/MesProductTjController.java
  13. 6 42
      src/main/resources/config/application.yml
  14. 15 0
      src/main/resources/mappings/modules/mes/MesProductTjDao.xml
  15. 91 0
      src/main/resources/views/modules/mes/mesProductTjForm.html
  16. 88 0
      src/main/resources/views/modules/mes/mesProductTjList.html
  17. 27 4
      src/main/resources/views/modules/mes/mesScreen3.html
  18. 131 32
      src/main/resources/views/modules/mes/mesScreen4.html
  19. 130 31
      src/main/resources/views/modules/mes/mesScreen5.html
  20. 162 45
      src/main/resources/views/modules/mes/mesScreen6.html

+ 15 - 0
mescloud-ljr.iml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module version="4">
+  <component name="FacetManager">
+    <facet type="web" name="Web2">
+      <configuration>
+        <descriptors>
+          <deploymentDescriptor name="web.xml" url="file://$MODULE_DIR$/src/main/webapp/WEB-INF/web.xml" />
+        </descriptors>
+        <webroots>
+          <root url="file://$MODULE_DIR$/src/main/webapp" relative="/" />
+        </webroots>
+      </configuration>
+    </facet>
+  </component>
+</module>

+ 15 - 0
src/main/java/com/jeesite/modules/mes/dao/MesProductTjDao.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.MesProductTj;
+
+/**
+ * 涂胶数据DAO接口
+ * @author liuwei
+ * @version 2026-08-14
+ */
+@MyBatisDao
+public interface MesProductTjDao extends CrudDao<MesProductTj> {
+	
+}

+ 10 - 0
src/main/java/com/jeesite/modules/mes/entity/MesDeviceAlarm.java

@@ -48,6 +48,8 @@ public class MesDeviceAlarm extends DataEntity<MesDeviceAlarm> {
 	private String content;		// 报警额外参数
 	private String state;		// 状态
 	private String remark;		// 备注
+	/** 从 deviceSn(如 RL_OP030_01)解析出的工位号,非表字段 */
+	private String oprno;
 
 	public MesDeviceAlarm() {
 		this(null);
@@ -126,6 +128,14 @@ public class MesDeviceAlarm extends DataEntity<MesDeviceAlarm> {
 	public void setRemark(String remark) {
 		this.remark = remark;
 	}
+
+	public String getOprno() {
+		return oprno;
+	}
+
+	public void setOprno(String oprno) {
+		this.oprno = oprno;
+	}
 	
 	public Date getCreateDate_gte() {
 		return sqlMap.getWhere().getValue("create_date", QueryType.GTE);

+ 96 - 0
src/main/java/com/jeesite/modules/mes/entity/MesProductTj.java

@@ -0,0 +1,96 @@
+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;
+
+/**
+ * 涂胶数据Entity
+ * @author liuwei
+ * @version 2026-08-14
+ */
+@Table(name="mes_product_tj", alias="a", label="涂胶数据信息", columns={
+		@Column(name="id", attrName="id", label="id", isPK=true),
+		@Column(name="sn", attrName="sn", label="工件码"),
+		@Column(name="line_sn", attrName="lineSn", label="产线编号"),
+		@Column(name="oprno", attrName="oprno", label="工位号"),
+		@Column(name="tj_speed", attrName="tjSpeed", label="涂胶速度", isUpdateForce=true),
+		@Column(name="tj_length", attrName="tjLength", label="涂胶长度", isUpdateForce=true),
+		@Column(name="tj_data", attrName="tjData", label="涂胶数据"),
+		@Column(name="create_by", attrName="createBy", label="create_by", isUpdate=false, isQuery=false),
+		@Column(name="create_date", attrName="createDate", label="上传时间", isUpdate=false, isQuery=false),
+	}, orderBy="a.id DESC"
+)
+public class MesProductTj extends DataEntity<MesProductTj> {
+	
+	private static final long serialVersionUID = 1L;
+	private String sn;		// 工件码
+	private String lineSn;		// 产线编号
+	private String oprno;		// 工位号
+	private Double tjSpeed;		// 涂胶速度
+	private Double tjLength;		// 涂胶长度
+	private String tjData;		// 涂胶数据 序号#速度#长度#...
+
+	public MesProductTj() {
+		this(null);
+	}
+	
+	public MesProductTj(String id){
+		super(id);
+	}
+	
+	@Size(min=0, max=255, message="工件码长度不能超过 255 个字符")
+	public String getSn() {
+		return sn;
+	}
+
+	public void setSn(String sn) {
+		this.sn = sn;
+	}
+	
+	@Size(min=0, max=255, message="产线编号长度不能超过 255 个字符")
+	public String getLineSn() {
+		return lineSn;
+	}
+
+	public void setLineSn(String lineSn) {
+		this.lineSn = lineSn;
+	}
+	
+	@Size(min=0, max=255, message="工位号长度不能超过 255 个字符")
+	public String getOprno() {
+		return oprno;
+	}
+
+	public void setOprno(String oprno) {
+		this.oprno = oprno;
+	}
+	
+	public Double getTjSpeed() {
+		return tjSpeed;
+	}
+
+	public void setTjSpeed(Double tjSpeed) {
+		this.tjSpeed = tjSpeed;
+	}
+	
+	public Double getTjLength() {
+		return tjLength;
+	}
+
+	public void setTjLength(Double tjLength) {
+		this.tjLength = tjLength;
+	}
+
+	@Size(min=0, max=2000, message="涂胶数据长度不能超过 2000 个字符")
+	public String getTjData() {
+		return tjData;
+	}
+
+	public void setTjData(String tjData) {
+		this.tjData = tjData;
+	}
+	
+}

+ 40 - 0
src/main/java/com/jeesite/modules/mes/service/MesDeviceAlarmService.java

@@ -1,10 +1,14 @@
 package com.jeesite.modules.mes.service;
 
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.StringUtils;
 import com.jeesite.common.service.CrudService;
 import com.jeesite.modules.mes.entity.MesDeviceAlarm;
 import com.jeesite.modules.mes.dao.MesDeviceAlarmDao;
@@ -128,5 +132,41 @@ public class MesDeviceAlarmService extends CrudService<MesDeviceAlarmDao, MesDev
 			super.update(alarm);
 		}
 	}
+
+	private static final Pattern OPRNO_IN_DEVICE_SN = Pattern.compile("OP\\d+", Pattern.CASE_INSENSITIVE);
+
+	/**
+	 * 从设备编号拆解工位号。规则:{产线}_{工位}_{序号},如 RL_OP030_01 → OP030
+	 */
+	public static String parseOprnoFromDeviceSn(String deviceSn) {
+		if (StringUtils.isBlank(deviceSn)) {
+			return "";
+		}
+		String s = deviceSn.trim().toUpperCase();
+		String[] parts = s.split("_");
+		for (String p : parts) {
+			if (p != null && p.matches("OP\\d+")) {
+				return p;
+			}
+		}
+		Matcher m = OPRNO_IN_DEVICE_SN.matcher(s);
+		if (m.find()) {
+			return m.group().toUpperCase();
+		}
+		return "";
+	}
+
+	/** 为列表结果填充 oprno(非表字段,供大屏按工序归类) */
+	public void fillOprnoFromDeviceSn(List<MesDeviceAlarm> list) {
+		if (list == null || list.isEmpty()) {
+			return;
+		}
+		for (MesDeviceAlarm alarm : list) {
+			if (alarm == null) {
+				continue;
+			}
+			alarm.setOprno(parseOprnoFromDeviceSn(alarm.getDeviceSn()));
+		}
+	}
 	
 }

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

@@ -1306,6 +1306,56 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 		return ret;
 	}
 
+	/**
+	 * 最近 N 小时上下线 OK(含当前小时,可跨天)。labels 为小时 0–23。
+	 */
+	public Map<String, Object> getRecentHourlyUpDownOkStats(String lineSn, String onlineOprno, String offlineOprno,
+															int hours) {
+		if (hours <= 0) {
+			hours = 12;
+		}
+		if (hours > 24) {
+			hours = 24;
+		}
+		Calendar endCal = Calendar.getInstance();
+		endCal.set(Calendar.MINUTE, 0);
+		endCal.set(Calendar.SECOND, 0);
+		endCal.set(Calendar.MILLISECOND, 0);
+		endCal.add(Calendar.HOUR_OF_DAY, 1);
+		Calendar startCal = (Calendar) endCal.clone();
+		startCal.add(Calendar.HOUR_OF_DAY, -hours);
+		Date start = startCal.getTime();
+		Date end = endCal.getTime();
+		boolean filterLineSn = StringUtils.isNotBlank(lineSn);
+		List<Map<String, Object>> upRows = mesProductRecordDao.countHourlyOkByLineAndOprnoPrefix(
+				lineSn, onlineOprno, start, end, filterLineSn);
+		List<Map<String, Object>> downRows = mesProductRecordDao.countHourlyOkByLineAndOprnoPrefix(
+				lineSn, offlineOprno, start, end, filterLineSn);
+		int[] up24 = new int[24];
+		int[] down24 = new int[24];
+		fillHourlyCounts(upRows, up24);
+		fillHourlyCounts(downRows, down24);
+		int[] up = new int[hours];
+		int[] down = new int[hours];
+		List<String> labels = new ArrayList<>();
+		Calendar cur = (Calendar) startCal.clone();
+		int maxVal = 4;
+		for (int i = 0; i < hours; i++) {
+			int h = cur.get(Calendar.HOUR_OF_DAY);
+			labels.add(String.valueOf(h));
+			up[i] = up24[h];
+			down[i] = down24[h];
+			maxVal = Math.max(maxVal, Math.max(up[i], down[i]));
+			cur.add(Calendar.HOUR_OF_DAY, 1);
+		}
+		Map<String, Object> ret = MapUtils.newHashMap();
+		ret.put("up", up);
+		ret.put("down", down);
+		ret.put("labels", labels);
+		ret.put("max", maxVal);
+		return ret;
+	}
+
 	public Map<String, Integer> getCurrentHourCapacityStats(String lineSn, List<String> oprnos) {
 		Map<String, Integer> ret = MapUtils.newHashMap();
 		if (StringUtils.isBlank(lineSn) || oprnos == null || oprnos.isEmpty()) {
@@ -1446,6 +1496,92 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 	}
 
 	/**
+	 * 上一班产能:根据班次生产计划管理中当前班的上一班时间段统计
+	 * 未配置班次或匹配不到上一班时,返回 0
+	 */
+	public Map<String, Integer> getPrevShiftCapacityStats(String lineSn, List<String> oprnos) {
+		Map<String, Integer> ret = MapUtils.newHashMap();
+		if (StringUtils.isBlank(lineSn) || oprnos == null || oprnos.isEmpty()) {
+			return ret;
+		}
+		MesShiftDataResp prevShift = mesShiftService.getPrevShift();
+		Date startTime = null;
+		Date endTime = null;
+		if (!ObjectUtils.isEmpty(prevShift)
+				&& StringUtils.isNotBlank(prevShift.getStart())
+				&& StringUtils.isNotBlank(prevShift.getEnd())) {
+			try {
+				SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+				startTime = sdf.parse(prevShift.getStart());
+				endTime = sdf.parse(prevShift.getEnd());
+			} catch (Exception e) {
+				startTime = null;
+				endTime = null;
+			}
+		}
+		for (String oprno : oprnos) {
+			if (StringUtils.isBlank(oprno)) {
+				continue;
+			}
+			String formatOprno = CommonUitl.formatOprno(oprno.trim());
+			int count = 0;
+			if (startTime != null && endTime != null) {
+				Long c = mesProductRecordDao.countHourCapacityByLineAndOprnoPrefix(
+						lineSn.trim(), formatOprno, startTime, endTime);
+				count = c == null ? 0 : c.intValue();
+			}
+			ret.put(formatOprno, count);
+		}
+		return ret;
+	}
+
+	/**
+	 * 固定两班制上一班产能:当前 08:00–20:00 则取昨日 20:00–今日 08:00;
+	 * 当前夜班则取当日 08:00–20:00
+	 */
+	public Map<String, Integer> getFixedTwoPrevShiftCapacityStats(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.add(Calendar.DAY_OF_MONTH, -1);
+			startCal.set(Calendar.HOUR_OF_DAY, 20);
+			endCal.set(Calendar.HOUR_OF_DAY, 8);
+		} else if (hour >= 20) {
+			startCal.set(Calendar.HOUR_OF_DAY, 8);
+			endCal.set(Calendar.HOUR_OF_DAY, 20);
+		} else {
+			startCal.add(Calendar.DAY_OF_MONTH, -1);
+			startCal.set(Calendar.HOUR_OF_DAY, 8);
+			endCal.add(Calendar.DAY_OF_MONTH, -1);
+			endCal.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) {

+ 81 - 0
src/main/java/com/jeesite/modules/mes/service/MesProductTjService.java

@@ -0,0 +1,81 @@
+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.MesProductTj;
+import com.jeesite.modules.mes.dao.MesProductTjDao;
+
+/**
+ * 涂胶数据Service
+ * @author liuwei
+ * @version 2026-08-14
+ */
+@Service
+public class MesProductTjService extends CrudService<MesProductTjDao, MesProductTj> {
+	
+	/**
+	 * 获取单条数据
+	 * @param mesProductTj
+	 * @return
+	 */
+	@Override
+	public MesProductTj get(MesProductTj mesProductTj) {
+		return super.get(mesProductTj);
+	}
+	
+	/**
+	 * 查询分页数据
+	 * @param mesProductTj 查询条件
+	 * @param mesProductTj.page 分页对象
+	 * @return
+	 */
+	@Override
+	public Page<MesProductTj> findPage(MesProductTj mesProductTj) {
+		return super.findPage(mesProductTj);
+	}
+	
+	/**
+	 * 查询列表数据
+	 * @param mesProductTj
+	 * @return
+	 */
+	@Override
+	public List<MesProductTj> findList(MesProductTj mesProductTj) {
+		return super.findList(mesProductTj);
+	}
+	
+	/**
+	 * 保存数据(插入或更新)
+	 * @param mesProductTj
+	 */
+	@Override
+	@Transactional
+	public void save(MesProductTj mesProductTj) {
+		super.save(mesProductTj);
+	}
+	
+	/**
+	 * 更新状态
+	 * @param mesProductTj
+	 */
+	@Override
+	@Transactional
+	public void updateStatus(MesProductTj mesProductTj) {
+		super.updateStatus(mesProductTj);
+	}
+	
+	/**
+	 * 删除数据
+	 * @param mesProductTj
+	 */
+	@Override
+	@Transactional
+	public void delete(MesProductTj mesProductTj) {
+		super.delete(mesProductTj);
+	}
+	
+}

+ 41 - 14
src/main/java/com/jeesite/modules/mes/service/MesShiftService.java

@@ -5,7 +5,9 @@ import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
+import java.util.Calendar;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -13,6 +15,7 @@ import java.util.Map;
 
 import com.jeesite.common.collect.ListUtils;
 import com.jeesite.common.lang.ObjectUtils;
+import com.jeesite.common.lang.StringUtils;
 import com.jeesite.modules.mes.entity.MesLine;
 import com.jeesite.modules.mes.entity.MesPlan;
 import com.jeesite.modules.mes.resp.MesShiftDataResp;
@@ -96,24 +99,49 @@ public class MesShiftService extends CrudService<MesShiftDao, MesShift> {
 
 	// 获取当前班次时间
 	public MesShiftDataResp getCurShift(){
+		return getShiftAt(LocalDateTime.now());
+	}
+
+	/**
+	 * 上一班次:当前班开始时间减 1 秒所在班次。未匹配到则返回 null。
+	 */
+	public MesShiftDataResp getPrevShift() {
+		MesShiftDataResp cur = getCurShift();
+		if (ObjectUtils.isEmpty(cur) || StringUtils.isBlank(cur.getStart())) {
+			return null;
+		}
+		try {
+			DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+			Date start = dateFormat.parse(cur.getStart());
+			Calendar cal = Calendar.getInstance();
+			cal.setTime(start);
+			cal.add(Calendar.SECOND, -1);
+			LocalDateTime at = LocalDateTime.ofInstant(cal.getTime().toInstant(), ZoneId.systemDefault());
+			return getShiftAt(at);
+		} catch (ParseException e) {
+			e.printStackTrace();
+			return null;
+		}
+	}
+
+	/**
+	 * 指定时刻所处班次(在用班次表 mes_shift)
+	 */
+	public MesShiftDataResp getShiftAt(LocalDateTime at) {
+		if (at == null) {
+			return null;
+		}
 		MesShiftDataResp mesShiftDataResp = new MesShiftDataResp();
 
-		// 获取当前日期时间
-		LocalDateTime now = LocalDateTime.now();
-		// 获取今天的日期
-		LocalDate today = now.toLocalDate();
-		// 获取明天的日期
+		LocalDate today = at.toLocalDate();
 		LocalDate tomorrow = today.plusDays(1);
-		// 获取昨天的日期
 		LocalDate yestoday = today.plusDays(-1);
-		// 自定义日期格式
 		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
 
 		String todayStr = today.format(formatter);
 		String tomorrowStr = tomorrow.format(formatter);
 		String yesTodayStr = yestoday.format(formatter);
 
-		// 获取在用班次信息
 		MesShift mesShift = new MesShift();
 		mesShift.setStatus("0");
 		List<MesShift> shifts = this.findList(mesShift);
@@ -124,18 +152,17 @@ public class MesShiftService extends CrudService<MesShiftDao, MesShift> {
 		DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
 		String curStartStr = "";
 		String curEndStr = "";
-		Date curDate = new Date();
+		Date atDate = Date.from(at.atZone(ZoneId.systemDefault()).toInstant());
 		MesShift curMesShift = null;
 		for (MesShift mesShift1:shifts){
 			String start = todayStr +" "+ mesShift1.getStart();
 			String end = todayStr +" "+ mesShift1.getEnd();
 
-			if(mesShift1.getNext() == 1){ // 跨天
-				int hour = now.getHour();
-				int minute = now.getMinute();
+			if(mesShift1.getNext() != null && mesShift1.getNext() == 1){ // 跨天
+				int hour = at.getHour();
+				int minute = at.getMinute();
 				String ho = String.valueOf(hour);
 				String me = String.valueOf(minute);
-				//
 				if(ho.length()==1){
 					ho = "0"+ho;
 				}
@@ -155,7 +182,7 @@ public class MesShiftService extends CrudService<MesShiftDao, MesShift> {
 				Date sdate = dateFormat.parse(start);
 				Date edate = dateFormat.parse(end);
 
-				if(sdate.getTime() <= curDate.getTime() && curDate.getTime() < edate.getTime()){
+				if(sdate.getTime() <= atDate.getTime() && atDate.getTime() < edate.getTime()){
 					curMesShift = CopyUtil.copy(mesShift1,MesShift.class);
 					curStartStr = start;
 					curEndStr = end;

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

@@ -388,7 +388,8 @@ public class MesProductController extends BaseController {
 	 *   <li>lineSn:产线编号,可选;不传或空则不过滤产线(全部产线汇总)</li>
 	 *   <li>queryDate:统计日期 yyyy-MM-dd,可选,默认当天</li>
 	 * </ul>
-	 * <p>返回 data:up、down 各 24 个 int(下标 0–23 对应 0–23 时),max 为纵轴建议最大值。</p>
+	 * <p>返回 data:up、down 各 24 个 int(下标 0–23 对应 0–23 时),max 为纵轴建议最大值。
+	 * 传 recentHours=12 时改为最近 N 小时(含当前小时,可跨天),并带 labels。</p>
 	 */
 	@RequestMapping(value = "hourlyUpDownOkData")
 	@ResponseBody
@@ -398,21 +399,32 @@ public class MesProductController extends BaseController {
 		String onlineOprno = trimRequestParam(request.getParameter("onlineOprno"));
 		String offlineOprno = trimRequestParam(request.getParameter("offlineOprno"));
 		String queryDate = request.getParameter("queryDate");
+		String recentHoursStr = trimRequestParam(request.getParameter("recentHours"));
 		if (StringUtils.isBlank(onlineOprno) || StringUtils.isBlank(offlineOprno)) {
 			resp.setResult(Global.FALSE);
 			resp.setMessage("onlineOprno、offlineOprno 不能为空");
 			return resp;
 		}
 		try {
-			LocalDate day = StringUtils.isNotBlank(queryDate)
-					? LocalDate.parse(queryDate.trim(), DateTimeFormatter.ISO_LOCAL_DATE)
-					: LocalDate.now();
-			java.time.LocalDateTime startLdt = day.atStartOfDay();
-			java.time.LocalDateTime endLdt = day.plusDays(1).atStartOfDay();
-			Date dayStart = Date.from(startLdt.atZone(ZoneId.systemDefault()).toInstant());
-			Date dayEnd = Date.from(endLdt.atZone(ZoneId.systemDefault()).toInstant());
-			Map<String, Object> data = mesProductRecordService.getHourlyUpDownOkStats(
-					lineSn, onlineOprno, offlineOprno, dayStart, dayEnd);
+			int recentHours = 0;
+			if (StringUtils.isNotBlank(recentHoursStr)) {
+				recentHours = Integer.parseInt(recentHoursStr);
+			}
+			Map<String, Object> data;
+			if (recentHours > 0) {
+				data = mesProductRecordService.getRecentHourlyUpDownOkStats(
+						lineSn, onlineOprno, offlineOprno, recentHours);
+			} else {
+				LocalDate day = StringUtils.isNotBlank(queryDate)
+						? LocalDate.parse(queryDate.trim(), DateTimeFormatter.ISO_LOCAL_DATE)
+						: LocalDate.now();
+				java.time.LocalDateTime startLdt = day.atStartOfDay();
+				java.time.LocalDateTime endLdt = day.plusDays(1).atStartOfDay();
+				Date dayStart = Date.from(startLdt.atZone(ZoneId.systemDefault()).toInstant());
+				Date dayEnd = Date.from(endLdt.atZone(ZoneId.systemDefault()).toInstant());
+				data = mesProductRecordService.getHourlyUpDownOkStats(
+						lineSn, onlineOprno, offlineOprno, dayStart, dayEnd);
+			}
 			resp.setData(data);
 			resp.setResult(Global.TRUE);
 		} catch (Exception e) {

+ 38 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java

@@ -480,6 +480,44 @@ public class MesProductRecordController extends BaseController {
 	}
 
 	/**
+	 * 上班产量(上一班次产能):
+	 * 默认按班次生产计划管理匹配上一班;传 fixedTwoShift=true 时用固定两班的上一班
+	 */
+	@RequestMapping(value = "prevShiftCapacityData")
+	@ResponseBody
+	public CommonResp<Map<String, Integer>> prevShiftCapacityData(HttpServletRequest request) {
+		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 不能为空");
+			return resp;
+		}
+		try {
+			List<String> oprnoList = new ArrayList<>();
+			String[] arr = oprnos.split(",");
+			for (String item : arr) {
+				String oprno = StringUtils.trimToEmpty(item);
+				if (StringUtils.isNotBlank(oprno)) {
+					oprnoList.add(oprno);
+				}
+			}
+			boolean useFixed = "true".equalsIgnoreCase(fixedTwoShift) || "1".equals(fixedTwoShift);
+			resp.setData(useFixed
+					? mesProductRecordService.getFixedTwoPrevShiftCapacityStats(lineSn, oprnoList)
+					: mesProductRecordService.getPrevShiftCapacityStats(lineSn, oprnoList));
+			resp.setResult("true");
+		} catch (Exception e) {
+			logger.error(e.getMessage(), e);
+			resp.setResult("false");
+			resp.setMessage("查询失败:" + e.getMessage());
+		}
+		return resp;
+	}
+
+	/**
 	 * 近七日产品质检数据分析(按产线)
 	 * 默认:产出=半成品入库,不良=返工全量
 	 * 可选 producedOprno:产出改为该工位 content=OK 按日计数

+ 5 - 2
src/main/java/com/jeesite/modules/mes/web/MesProductScreenController.java

@@ -122,7 +122,8 @@ public class MesProductScreenController extends BaseController {
 		return resp;
 	}
 
-	/** 大屏:待处理设备报警列表(替代 mesDeviceAlarm/listData) */
+	/** 大屏:待处理设备报警列表(替代 mesDeviceAlarm/listData)
+	 * 解析 deviceSn(如 RL_OP030_01)中间工位号写入 oprno,便于按工序归类展示 */
 	@RequestMapping(value = "alertList")
 	@ResponseBody
 	public Page<MesDeviceAlarm> alertList(MesDeviceAlarm mesDeviceAlarm,
@@ -131,7 +132,9 @@ public class MesProductScreenController extends BaseController {
 			mesDeviceAlarm.setState("0");
 		}
 		mesDeviceAlarm.setPage(new Page<>(request, response));
-		return mesDeviceAlarmService.findPage(mesDeviceAlarm);
+		Page<MesDeviceAlarm> page = mesDeviceAlarmService.findPage(mesDeviceAlarm);
+		mesDeviceAlarmService.fillOprnoFromDeviceSn(page.getList());
+		return page;
 	}
 
 	/** 大屏:当天返修/报废数量(替代 mesRepair/listData + mesScrap/listData 计数) */

+ 145 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductTjController.java

@@ -0,0 +1,145 @@
+package com.jeesite.modules.mes.web;
+
+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.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 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.MesProductTj;
+import com.jeesite.modules.mes.resp.CommonResp;
+import com.jeesite.modules.mes.service.MesProductTjService;
+
+/**
+ * 涂胶数据Controller
+ * @author liuwei
+ * @version 2026-08-14
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/mesProductTj")
+public class MesProductTjController extends BaseController {
+
+	@Autowired
+	private MesProductTjService mesProductTjService;
+	
+	/**
+	 * 获取数据
+	 */
+	@ModelAttribute
+	public MesProductTj get(String id, boolean isNewRecord) {
+		return mesProductTjService.get(id, isNewRecord);
+	}
+	
+	/**
+	 * 查询列表
+	 */
+	@RequiresPermissions("mes:mesProductTj:view")
+	@RequestMapping(value = {"list", ""})
+	public String list(MesProductTj mesProductTj, Model model) {
+		model.addAttribute("mesProductTj", mesProductTj);
+		return "modules/mes/mesProductTjList";
+	}
+	
+	/**
+	 * 查询列表数据
+	 */
+	@RequiresPermissions("mes:mesProductTj:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MesProductTj> listData(MesProductTj mesProductTj, HttpServletRequest request, HttpServletResponse response) {
+		mesProductTj.setPage(new Page<>(request, response));
+		Page<MesProductTj> page = mesProductTjService.findPage(mesProductTj);
+		return page;
+	}
+
+	/**
+	 * 查看编辑表单
+	 */
+	@RequiresPermissions("mes:mesProductTj:view")
+	@RequestMapping(value = "form")
+	public String form(MesProductTj mesProductTj, Model model) {
+		model.addAttribute("mesProductTj", mesProductTj);
+		return "modules/mes/mesProductTjForm";
+	}
+
+	/**
+	 * 保存数据
+	 */
+	@RequiresPermissions("mes:mesProductTj:edit")
+	@PostMapping(value = "save")
+	@ResponseBody
+	public String save(@Validated MesProductTj mesProductTj) {
+		mesProductTjService.save(mesProductTj);
+		return renderResult(Global.TRUE, text("保存涂胶数据成功!"));
+	}
+	
+	/**
+	 * 删除数据
+	 */
+	@RequiresPermissions("mes:mesProductTj:edit")
+	@RequestMapping(value = "delete")
+	@ResponseBody
+	public String delete(MesProductTj mesProductTj) {
+		mesProductTjService.delete(mesProductTj);
+		return renderResult(Global.TRUE, text("删除涂胶数据成功!"));
+	}
+
+	/**
+	 * 设备端上传涂胶长串(序号#速度#长度#...),整段写入 tj_data
+	 */
+	@ResponseBody
+	@PostMapping(value = "upload")
+	public CommonResp<String> upload(HttpServletRequest request) {
+		CommonResp<String> resp = new CommonResp<>();
+		String sn = request.getParameter("sn");
+		String lineSn = request.getParameter("lineSn");
+		String oprno = request.getParameter("oprno");
+		String tjData = request.getParameter("tjData");
+		if (StringUtils.isBlank(sn) || StringUtils.isBlank(lineSn) || StringUtils.isBlank(oprno)) {
+			resp.setResult(Global.FALSE);
+			resp.setMessage("参数错误");
+			return resp;
+		}
+		if (StringUtils.isNotBlank(tjData)) {
+			tjData = tjData.trim();
+			String[] parts = tjData.split("#", -1);
+			if (parts.length % 3 != 0) {
+				resp.setResult(Global.FALSE);
+				resp.setMessage("涂胶数据格式错误,应为 序号#速度#长度#...");
+				return resp;
+			}
+			if (tjData.length() > 2000) {
+				resp.setResult(Global.FALSE);
+				resp.setMessage("涂胶数据超长");
+				return resp;
+			}
+		}
+		try {
+			MesProductTj entity = new MesProductTj();
+			entity.setSn(sn.trim());
+			entity.setLineSn(lineSn.trim());
+			entity.setOprno(oprno.trim());
+			entity.setTjData(tjData);
+			mesProductTjService.save(entity);
+			resp.setResult(Global.TRUE);
+			resp.setMessage("上传成功");
+		} catch (Exception e) {
+			logger.error("涂胶数据上传失败 sn={}", sn, e);
+			resp.setResult(Global.FALSE);
+			resp.setMessage("上传失败");
+		}
+		return resp;
+	}
+	
+}

+ 6 - 42
src/main/resources/config/application.yml

@@ -671,7 +671,6 @@ shiro:
     ${adminPath}/mes/mesProductRecord/check = anon
     ${adminPath}/mes/mesProductRecord/getConfig = anon
     ${adminPath}/mes/mesElectricTorque/batchSave = anon
-    ${adminPath}/mes/mesScrew/batchSave = anon
     ${adminPath}/mes/mesProductYgsl/batchsave = anon
     ${adminPath}/mes/mesProductHb/upload = anon
     ${adminPath}/mes/mesProductCmt/batchsave = anon
@@ -754,46 +753,11 @@ shiro:
     ${adminPath}/mes/mesProductCncLife/saveLife= anon
     ${adminPath}/mes/mesProductCncParam/batchsave= anon
     ${adminPath}/mes/mesProductCncToolGeo/saveToolGeo= anon
-    ${adminPath}/mes/mesProduct/screen= anon
-    ${adminPath}/mes/mesProduct/screen3= anon
-    ${adminPath}/mes/mesProduct/screen5= anon
-    ${adminPath}/mes/mesProduct/screen6= anon
-    ${adminPath}/mes/mesProduct/hourlyUpDownOkData = anon
-    ${adminPath}/mes/mesProduct/defectTypeData= anon
-    ${adminPath}/mes/mesProductScreen/latestConfig= anon
-    ${adminPath}/mes/mesDeviceAlarm/listData= anon
-    ${adminPath}/mes/mesRepair/listData= anon
-    ${adminPath}/mes/mesScrap/listData= anon
-    ${adminPath}/mes/mesLineProcess/listData= anon
-    ${adminPath}/mes/mesProductRecord/weekQualityData= anon
+    ${adminPath}/mes/mesProductRecord/hourlyCapacityData= anon
     ${adminPath}/mes/mesProductRecord/dayCapacityData= anon
     ${adminPath}/mes/mesProductRecord/shiftCapacityData= anon
-    ${adminPath}/mes/mesProductRecord/monthlyProductData= anon
-    ${adminPath}/mes/mesProductRecord/listData= anon
-    ${adminPath}/mes/mesPlan/latestPlanNums= anon
-    ${adminPath}/mes/mesDevice/listData= anon
-    ${adminPath}/sys/empUser/listData= anon
-    ${adminPath}/mes/mesProductVoltage/add= anon
-    
-    ${adminPath}/mes/mesProduct/screen = anon
-    ${adminPath}/mes/mesProduct/screen3 = anon
-    ${adminPath}/mes/mesProduct/screen5 = anon
-    ${adminPath}/mes/mesProduct/screen6 = anon
-    ${adminPath}/mes/mesProduct/hourlyUpDownOkData = anon
-    ${adminPath}/mes/mesProduct/defectTypeData = anon
-    ${adminPath}/mes/mesProductScreen/latestConfig = anon
-    ${adminPath}/mes/mesProductScreen/alertList = anon
-    ${adminPath}/mes/mesProductScreen/repairScrapCount = anon
-    ${adminPath}/mes/mesProductScreen/lineProcessList = anon
-    ${adminPath}/mes/mesProductScreen/recordList = anon
-    ${adminPath}/mes/mesProductScreen/peopleCount = anon
-    ${adminPath}/mes/mesProductScreen/deviceCount = anon
-    ${adminPath}/mes/mesProductRecord/dayCapacityData = anon
-    ${adminPath}/mes/mesProductRecord/shiftCapacityData = anon
-    ${adminPath}/mes/mesProductRecord/weekQualityData = anon
-    ${adminPath}/mes/mesProductRecord/monthlyProductData = anon
-    ${adminPath}/mes/mesPlan/latestPlanNums = anon
-
+    ${adminPath}/mes/mesProductRecord/prevShiftCapacityData= anon
+    ${adminPath}/mes/mesProduct/hourlyUpDownOkData= anon
     
     
   
@@ -1133,9 +1097,9 @@ mes:
       password: oPCuA_123
     # FSW Modbus TCP 握手(工件码质量校验 + 过程参数采集)
     modbus:
-      enabled: true
+      enabled: false
       host: 10.0.20.220
       port: 502
       unitId: 1
-      oprno:  OP230        # 工位号,必填,例如 OP210A
-      lineSn:   XT      # 产线编号,可空(空则按工件类型自动解析)
+      oprno:          # 工位号,必填,例如 OP210A
+      lineSn:         # 产线编号,可空(空则按工件类型自动解析)

+ 15 - 0
src/main/resources/mappings/modules/mes/MesProductTjDao.xml

@@ -0,0 +1,15 @@
+<?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.MesProductTjDao">
+	
+	<!-- 查询数据
+	<select id="findList" resultType="MesProductTj">
+		SELECT ${sqlMap.column.toSql()}
+		FROM ${sqlMap.table.toSql()}
+		<where>
+			${sqlMap.where.toSql()}
+		</where>
+		ORDER BY ${sqlMap.order.toSql()}
+	</select> -->
+	
+</mapper>

+ 91 - 0
src/main/resources/views/modules/mes/mesProductTjForm.html

@@ -0,0 +1,91 @@
+<% 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(mesProductTj.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="${mesProductTj}" action="${ctx}/mes/mesProductTj/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 hide">*</span> ${text('工件码')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-8">
+								<#form:input path="sn" maxlength="255" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4" title="">
+								<span class="required hide">*</span> ${text('产线编号')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-8">
+								<#form:input path="lineSn" maxlength="255" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4" title="">
+								<span class="required hide">*</span> ${text('工位号')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-8">
+								<#form:input path="oprno" maxlength="64" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4" title="">
+								<span class="required hide">*</span> ${text('涂胶速度')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-8">
+								<#form:input path="tjSpeed" class="form-control number"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-12">
+						<div class="form-group">
+							<label class="control-label col-sm-2" title="">
+								<span class="required hide">*</span> ${text('涂胶数据')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-10">
+								<#form:textarea path="tjData" rows="3" maxlength="2000" class="form-control" placeholder="序号#速度#长度#..."/>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div class="box-footer">
+				<div class="row">
+					<div class="col-sm-offset-2 col-sm-10">
+						<% if (hasPermi('mes:mesProductTj: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>

+ 88 - 0
src/main/resources/views/modules/mes/mesProductTjList.html

@@ -0,0 +1,88 @@
+<% 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>
+				<% if(hasPermi('mes:mesProductTj:edit')){ %>
+					<a href="${ctx}/mes/mesProductTj/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="${mesProductTj}" action="${ctx}/mes/mesProductTj/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="sn" 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="lineSn" 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="oprno" 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="tjSpeed" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('涂胶数据')}:</label>
+					<div class="control-inline">
+						<#form:input path="tjData" maxlength="2000" 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').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("工件码")}', name:'sn', index:'a.sn', width:150, align:"left", frozen:true, formatter: function(val, obj, row, act){
+			return '<a href="${ctx}/mes/mesProductTj/form?id='+row.id+'" class="btnList" data-title="${text("编辑涂胶数据")}">'+(val||row.id)+'</a>';
+		}},
+		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:150, align:"left"},
+		{header:'${text("工位号")}', name:'oprno', index:'a.oprno', width:150, align:"center"},
+		{header:'${text("涂胶速度")}', name:'tjSpeed', index:'a.tj_speed', width:150, align:"right", formatter: function(val, obj, row, act){
+			return js.formatNumber(val, 2, false, ''); // 数值类型格式化 (原始数值, 小数位数, 是否千分位, 默认值,金额情况下设置0.00);
+		}},
+		{header:'${text("涂胶数据")}', name:'tjData', index:'a.tj_data', width:280, align:"left"},
+		{header:'${text("操作")}', name:'actions', width:120, formatter: function(val, obj, row, act){
+			var actions = [];
+			//<% if(hasPermi('mes:mesProductTj:edit')){ %>
+				actions.push('<a href="${ctx}/mes/mesProductTj/form?id='+row.id+'" class="btnList" title="${text("编辑涂胶数据")}"><i class="fa fa-pencil"></i></a>&nbsp;');
+				actions.push('<a href="${ctx}/mes/mesProductTj/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>

+ 27 - 4
src/main/resources/views/modules/mes/mesScreen3.html

@@ -1486,6 +1486,23 @@
 		return [];
 	}
 
+	function parseDeviceSnParts(deviceSn) {
+		var s = String(deviceSn || '').trim().toUpperCase();
+		var parts = s.split('_').filter(function(p) { return !!p; });
+		var line = parts.length > 0 ? parts[0] : '';
+		var oprno = '';
+		for (var i = 0; i < parts.length; i++) {
+			if (/^OP\d+$/.test(parts[i])) {
+				oprno = parts[i];
+				break;
+			}
+		}
+		if (!oprno) {
+			var m = s.match(/OP\d+/);
+			if (m) oprno = m[0];
+		}
+		return { line: line, oprno: oprno };
+	}
 	function updateDeviceAlarmAlertsByApi() {
 		$.ajax({
 			url: '${ctx}/mes/mesProductScreen/alertList',
@@ -1493,18 +1510,24 @@
 			dataType: 'json',
 			data: {
 				pageNo: 1,
-				pageSize: 50,
-				state: '0' // 只显示待处理的报警
+				pageSize: 200,
+				state: '0'
 			},
 			success: function(resp) {
 				var rows = extractList(resp);
 				var alerts = [];
 				for (var i = 0; i < rows.length; i++) {
 					var r = rows[i] || {};
-					// 双保险:返回数据里如果不是“待处理”的,跳过
 					if (String(r.state) !== '0') continue;
+					var sn = r.deviceSn || r.device_sn || r.deviceCode || r.device_code || '';
+					var parts = parseDeviceSnParts(sn);
+					var oprno = String(r.oprno || parts.oprno || '').toUpperCase();
 					var typ = (r.type != null && r.type !== undefined) ? String(r.type) : '';
-					alerts.push({ no: alerts.length + 1, text: typ });
+					var text = oprno
+						? (oprno + (typ ? (' | ' + typ) : ''))
+						: typ;
+					if (!text) continue;
+					alerts.push({ no: alerts.length + 1, text: text, oprno: oprno || '' });
 				}
 				mockData.alerts = alerts;
 				renderAlerts();

+ 131 - 32
src/main/resources/views/modules/mes/mesScreen4.html

@@ -303,7 +303,7 @@
 	<div class="mes4-panel p-alert">
 		<div class="mes4-ptitle">  产线阶段报警信息  </div>
 		<div class="mes4-pbody">
-			<div class="alert-head"><div>序号</div><div>报警类型</div></div>
+			<div class="alert-head"><div>序号</div><div>工位 / 报警类型</div></div>
 			<div id="alertList"></div>
 		</div>
 	</div>
@@ -331,6 +331,7 @@
 					<th style="width:300px">工位名称</th>
 					<th>实际产能</th>
 					<th>当班产能</th>
+					<th>上班产量</th>
 				</tr>
 				</thead>
 				<tbody id="opTableBody"></tbody>
@@ -379,7 +380,7 @@
 	</div>
 
 	<div class="mes4-panel p-hourly">
-		<div class="mes4-ptitle">  每小时上下线数据分析  </div>
+		<div class="mes4-ptitle">  近12小时上下线数据分析  </div>
 		<div class="legend-inline">
 			<span><i class="dot" style="background:#00FF88"></i>上线</span>
 			<span><i class="dot" style="background:#00C8FF"></i>下线</span>
@@ -425,12 +426,13 @@
 	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++) {
-			hourLabels.push(pad(i));
+	function buildRecentHourLabels12() {
+		var labels = [];
+		var nowH = new Date().getHours();
+		for (var i = 11; i >= 0; i--) {
+			labels.push(String((nowH - i + 24) % 24));
 		}
-		return hourLabels;
+		return labels;
 	}
 	function updateHourlyUpDownByApi() {
 		$.ajax({
@@ -440,19 +442,20 @@
 			data: {
 				lineSn: HOURLY_API_CFG.lineSn,
 				onlineOprno: HOURLY_API_CFG.onlineOprno,
-				offlineOprno: HOURLY_API_CFG.offlineOprno
+				offlineOprno: HOURLY_API_CFG.offlineOprno,
+				recentHours: 12
 			},
 			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) {
+					if (!d.up || !d.down || d.up.length < 1) {
 						return;
 					}
 					mockData.hourly = {
-						labels: buildHourLabels24(),
+						labels: d.labels && d.labels.length ? d.labels : buildRecentHourLabels12(),
 						up: d.up,
 						down: d.down,
-						max: typeof d.max === 'number' ? d.max : 100
+						max: 4
 					};
 					drawAllCharts();
 				}
@@ -507,14 +510,13 @@
 			max: 1300, min: 1000
 		};
 
-		// 每小时上下线 (24 小时)
-		var hourLabels = [], up = [], down = [];
-		for (var i = 1; i <= 24; i++) {
-			hourLabels.push(pad(i));
-			up.push(Math.round(60 + Math.random() * 30));
-			down.push(Math.round(50 + Math.random() * 30));
+		// 近12小时上下线
+		var hourLabels = buildRecentHourLabels12(), up = [], down = [];
+		for (var i = 0; i < 12; i++) {
+			up.push(Math.round(Math.random() * 4));
+			down.push(Math.round(Math.random() * 4));
 		}
-		mockData.hourly = { labels: hourLabels, up: up, down: down, max: 100 };
+		mockData.hourly = { labels: hourLabels, up: up, down: down, max: 4 };
 
 		// 各月产量
 		mockData.monthly = {
@@ -557,7 +559,7 @@
 		}
 	}
 
-	// 双柱状图 - 近七日产品质检 (产出/不良)
+	// 双柱状图 - 近七日产品质检 (产出/不良) / 每小时上下线
 	function drawDoubleBar(id, a, b, labels, maxV) {
 		var p = prepareCanvas(id); if (!p) return;
 		var ctx = p.ctx, w = p.w, h = p.h;
@@ -566,24 +568,80 @@
 		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.28;
-		for (var i = 0; i < a.length; i++) {
+		var n = a.length;
+		var dense = n > 12;
+		var gw = cw / n, bw = gw * (dense ? 0.32 : 0.28);
+		var labelStep = dense ? 2 : 1;
+		var valFont = dense ? 'bold 10px Microsoft YaHei' : 'bold 12px Microsoft YaHei';
+		var lblFont = dense ? '11px Microsoft YaHei' : '13px Microsoft YaHei';
+		for (var i = 0; i < n; i++) {
 			var cx = padL + i * gw + gw / 2;
 			var h1 = (a[i] / maxV) * ch;
 			var g1 = ctx.createLinearGradient(0, padT + ch - h1, 0, padT + ch);
 			g1.addColorStop(0, '#00FF88'); g1.addColorStop(1, 'rgba(0,255,136,0.15)');
 			ctx.fillStyle = g1;
 			ctx.fillRect(cx - bw - 2, padT + ch - h1, bw, h1);
-			var h2 = (b[i] / maxV) * ch;
+			var h2 = ((b && b[i] != null ? b[i] : 0) / maxV) * ch;
 			var g2 = ctx.createLinearGradient(0, padT + ch - h2, 0, padT + ch);
 			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 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.95)'; ctx.font = '13px Microsoft YaHei';
-			ctx.fillText(labels[i], cx, h - 8);
+			if (!dense || a[i] > 0) {
+				ctx.fillStyle = '#00FF88'; ctx.font = valFont;
+				ctx.textAlign = 'center'; ctx.fillText(a[i], cx - bw / 2 - 2, padT + ch - h1 - 4);
+			}
+			if (!dense || (b && b[i] > 0)) {
+				ctx.fillStyle = '#00C8FF'; ctx.font = valFont;
+				ctx.textAlign = 'center';
+				ctx.fillText(b && b[i] != null ? b[i] : 0, cx + bw / 2 + 2, padT + ch - h2 - 4);
+			}
+			if (i % labelStep === 0) {
+				ctx.fillStyle = 'rgba(77,201,245,0.95)'; ctx.font = lblFont;
+				ctx.textAlign = 'center';
+				ctx.fillText(labels[i], cx, h - 8);
+			}
+		}
+	}
+
+	// 近12小时上下线双柱:纵轴 0–4,横坐标/柱顶数值与柱组中心对齐
+	function drawHourlyDoubleBar(id, a, b, labels) {
+		var p = prepareCanvas(id); if (!p) return;
+		var ctx = p.ctx, w = p.w, h = p.h;
+		var maxV = 4;
+		var padL = 36, padR = 10, padT = 18, 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 n = a.length;
+		var gw = cw / n;
+		var bw = Math.max(3, Math.min(14, gw * 0.28));
+		var gap = 2;
+		ctx.textAlign = 'center';
+		for (var i = 0; i < n; i++) {
+			var cx = padL + (i + 0.5) * gw;
+			var v1 = Number(a[i]) || 0;
+			var v2 = Number(b && b[i] != null ? b[i] : 0) || 0;
+			var h1 = (Math.min(v1, maxV) / maxV) * ch;
+			var h2 = (Math.min(v2, maxV) / maxV) * ch;
+			var x1 = cx - gap / 2 - bw;
+			var x2 = cx + gap / 2;
+			var g1 = ctx.createLinearGradient(0, padT + ch - h1, 0, padT + ch);
+			g1.addColorStop(0, '#00FF88'); g1.addColorStop(1, 'rgba(0,255,136,0.15)');
+			ctx.fillStyle = g1;
+			ctx.fillRect(x1, padT + ch - h1, bw, h1);
+			var g2 = ctx.createLinearGradient(0, padT + ch - h2, 0, padT + ch);
+			g2.addColorStop(0, '#00C8FF'); g2.addColorStop(1, 'rgba(0,200,255,0.15)');
+			ctx.fillStyle = g2;
+			ctx.fillRect(x2, padT + ch - h2, bw, h2);
+			ctx.font = 'bold 11px Microsoft YaHei';
+			ctx.fillStyle = '#00FF88';
+			ctx.fillText(String(v1), x1 + bw / 2, padT + ch - h1 - 4);
+			ctx.fillStyle = '#00C8FF';
+			ctx.fillText(String(v2), x2 + bw / 2, padT + ch - h2 - 4);
+			ctx.fillStyle = 'rgba(77,201,245,0.95)';
+			ctx.font = '12px Microsoft YaHei';
+			ctx.fillText(labels[i] != null ? String(labels[i]) : String(i), cx, h - 8);
 		}
 	}
 
@@ -855,6 +913,7 @@
 			html += '<td class="op-name">' + r.name + '</td>';
 			html += '<td class="op-val">' + r.real + '</td>';
 			html += '<td class="op-val">' + r.shift + '</td>';
+			html += '<td class="op-val">' + (r.prevShift || 0) + '</td>';
 			html += '</tr>';
 		}
 		var el = document.getElementById('opTableBody');
@@ -904,6 +963,7 @@
 						name: p.title || '',
 						real: 0,
 						shift: 0,
+						prevShift: 0,
 						_opNum: parseOprnoNum(code)
 					});
 				}
@@ -913,6 +973,7 @@
 				if (out.length > 0) {
 					updateHourlyCapacityByApi();
 				}
+				updateDeviceAlarmAlertsByApi();
 			}
 		});
 	}
@@ -927,6 +988,7 @@
 		if (!oprnos.length) return;
 		fetchCapacityInto('dayCapacityData', LINE_SN, oprnos, 'real', {});
 		fetchCapacityInto('shiftCapacityData', LINE_SN, oprnos, 'shift', { fixedTwoShift: 'true' });
+		fetchCapacityInto('prevShiftCapacityData', LINE_SN, oprnos, 'prevShift', { fixedTwoShift: 'true' });
 	}
 
 	function fetchCapacityInto(action, lineSn, oprnos, field, extraData) {
@@ -1005,6 +1067,36 @@
 		return [];
 	}
 
+	function parseDeviceSnParts(deviceSn) {
+		var s = String(deviceSn || '').trim().toUpperCase();
+		var parts = s.split('_').filter(function(p) { return !!p; });
+		var line = parts.length > 0 ? parts[0] : '';
+		var oprno = '';
+		for (var i = 0; i < parts.length; i++) {
+			if (/^OP\d+$/.test(parts[i])) {
+				oprno = parts[i];
+				break;
+			}
+		}
+		if (!oprno) {
+			var m = s.match(/OP\d+/);
+			if (m) oprno = m[0];
+		}
+		return { line: line, oprno: oprno };
+	}
+	function resolveAlertProcessName(oprno) {
+		var code = String(oprno || '').toUpperCase();
+		var num = parseOprnoNum(code);
+		var list = mockData.opTable || [];
+		for (var i = 0; i < list.length; i++) {
+			var c = String((list[i] && list[i].code) || '').toUpperCase();
+			if (!c) continue;
+			if (c === code || (num >= 0 && parseOprnoNum(c) === num)) {
+				return list[i].name || '';
+			}
+		}
+		return '';
+	}
 	function updateDeviceAlarmAlertsByApi() {
 		$.ajax({
 			url: '${ctx}/mes/mesProductScreen/alertList',
@@ -1012,17 +1104,24 @@
 			dataType: 'json',
 			data: {
 				pageNo: 1,
-				pageSize: 50,
-				state: '0' // 只显示待处理的报警
+				pageSize: 200,
+				state: '0'
 			},
 			success: function(resp) {
 				var rows = extractUserList(resp);
 				var alerts = [];
 				for (var i = 0; i < rows.length; i++) {
 					var r = rows[i] || {};
-					if (String(r.state) !== '0') continue; // 双保险:跳过非待处理
+					if (String(r.state) !== '0') continue;
+					var sn = r.deviceSn || r.device_sn || r.deviceCode || r.device_code || '';
+					var parts = parseDeviceSnParts(sn);
+					var oprno = String(r.oprno || parts.oprno || '').toUpperCase();
+					if (!oprno) continue;
+					if (!isOprnoInRange(oprno, 10, 100)) continue;
 					var typ = (r.type != null && r.type !== undefined) ? String(r.type) : '';
-					alerts.push({ no: alerts.length + 1, text: typ });
+					var pname = resolveAlertProcessName(oprno);
+					var text = oprno + (pname ? (' ' + pname) : '') + (typ ? (' | ' + typ) : '');
+					alerts.push({ no: alerts.length + 1, text: text, oprno: oprno });
 				}
 				mockData.alerts = alerts;
 				renderAlerts();
@@ -1110,7 +1209,7 @@
 		var q = mockData.qualityInspect;
 		if (q) drawDoubleBar('chartQuality', q.produced, q.defect, q.labels, q.max);
 		var hr = mockData.hourly;
-		if (hr) drawAreaLine('chartHourly', hr.up, hr.down, hr.labels, hr.max);
+		if (hr) drawHourlyDoubleBar('chartHourly', hr.up, hr.down, hr.labels);
 	}
 
 	function refreshData() {

+ 130 - 31
src/main/resources/views/modules/mes/mesScreen5.html

@@ -320,7 +320,7 @@
 	<div class="mes4-panel p-alert">
 		<div class="mes4-ptitle">  产线阶段报警信息  </div>
 		<div class="mes4-pbody">
-			<div class="alert-head"><div>序号</div><div>报警类型</div></div>
+			<div class="alert-head"><div>序号</div><div>工位 / 报警类型</div></div>
 			<div id="alertList"></div>
 		</div>
 	</div>
@@ -349,6 +349,7 @@
 					<th style="width:300px">工位名称</th>
 					<th>实际产能</th>
 					<th>当班产能</th>
+					<th>上班产量</th>
 				</tr>
 				</thead>
 				<tbody id="opTableBody"></tbody>
@@ -397,7 +398,7 @@
 	</div>
 
 	<div class="mes4-panel p-hourly">
-		<div class="mes4-ptitle">  每小时上下线数据分析  </div>
+		<div class="mes4-ptitle">  近12小时上下线数据分析  </div>
 		<div class="legend-inline">
 			<span><i class="dot" style="background:#00FF88"></i>上线</span>
 			<span><i class="dot" style="background:#00C8FF"></i>下线</span>
@@ -435,7 +436,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="4">&nbsp;</td></tr>';
+			html += '<tr class="op-row-empty' + alt + '"><td colspan="5">&nbsp;</td></tr>';
 		}
 		return html;
 	}
@@ -491,10 +492,11 @@
 	var OP_TO = 160;
 	/** 每小时上下线:上线 OP110 / 下线 OP160 */
 	var HOURLY_API_CFG = { lineSn: LINE_SN, onlineOprno: 'OP110', offlineOprno: 'OP160' };
-	function buildHourLabels24() {
+	function buildRecentHourLabels12() {
 		var labels = [];
-		for (var i = 1; i <= 24; i++) {
-			labels.push(pad(i));
+		var nowH = new Date().getHours();
+		for (var i = 11; i >= 0; i--) {
+			labels.push(String((nowH - i + 24) % 24));
 		}
 		return labels;
 	}
@@ -506,19 +508,20 @@
 			data: {
 				lineSn: HOURLY_API_CFG.lineSn,
 				onlineOprno: HOURLY_API_CFG.onlineOprno,
-				offlineOprno: HOURLY_API_CFG.offlineOprno
+				offlineOprno: HOURLY_API_CFG.offlineOprno,
+				recentHours: 12
 			},
 			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) {
+					if (!d.up || !d.down || d.up.length < 1) {
 						return;
 					}
 					mockData.hourly = {
-						labels: buildHourLabels24(),
+						labels: d.labels && d.labels.length ? d.labels : buildRecentHourLabels12(),
 						up: d.up,
 						down: d.down,
-						max: typeof d.max === 'number' ? d.max : 100
+						max: 4
 					};
 					drawAllCharts();
 				}
@@ -573,14 +576,13 @@
 			max: 1300, min: 1000
 		};
 
-		// 每小时上下线 (24 小时)
-		var hourLabels = [], up = [], down = [];
-		for (var i = 1; i <= 24; i++) {
-			hourLabels.push(pad(i));
-			up.push(Math.round(60 + Math.random() * 30));
-			down.push(Math.round(50 + Math.random() * 30));
+		// 近12小时上下线
+		var hourLabels = buildRecentHourLabels12(), up = [], down = [];
+		for (var i = 0; i < 12; i++) {
+			up.push(Math.round(Math.random() * 4));
+			down.push(Math.round(Math.random() * 4));
 		}
-		mockData.hourly = { labels: hourLabels, up: up, down: down, max: 100 };
+		mockData.hourly = { labels: hourLabels, up: up, down: down, max: 4 };
 
 		// 各月产量
 		mockData.monthly = {
@@ -623,7 +625,7 @@
 		}
 	}
 
-	// 双柱状图 - 近七日产品质检 (产出/不良)
+	// 双柱状图 - 近七日产品质检 (产出/不良) / 每小时上下线
 	function drawDoubleBar(id, a, b, labels, maxV) {
 		var p = prepareCanvas(id); if (!p) return;
 		var ctx = p.ctx, w = p.w, h = p.h;
@@ -632,24 +634,80 @@
 		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;
-		for (var i = 0; i < a.length; i++) {
+		var n = a.length;
+		var dense = n > 12;
+		var gw = cw / n, bw = gw * (dense ? 0.32 : 0.3);
+		var labelStep = dense ? 2 : 1;
+		var valFont = dense ? 'bold 9px Microsoft YaHei' : 'bold 10px Microsoft YaHei';
+		var lblFont = dense ? '10px Microsoft YaHei' : '11px Microsoft YaHei';
+		for (var i = 0; i < n; i++) {
 			var cx = padL + i * gw + gw / 2;
 			var h1 = (a[i] / maxV) * ch;
 			var g1 = ctx.createLinearGradient(0, padT + ch - h1, 0, padT + ch);
 			g1.addColorStop(0, '#00FF88'); g1.addColorStop(1, 'rgba(0,255,136,0.15)');
 			ctx.fillStyle = g1;
 			ctx.fillRect(cx - bw - 2, padT + ch - h1, bw, h1);
-			var h2 = (b[i] / maxV) * ch;
+			var h2 = ((b && b[i] != null ? b[i] : 0) / maxV) * ch;
 			var g2 = ctx.createLinearGradient(0, padT + ch - h2, 0, padT + ch);
 			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.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);
+			if (!dense || a[i] > 0) {
+				ctx.fillStyle = '#00FF88'; ctx.font = valFont;
+				ctx.textAlign = 'center'; ctx.fillText(a[i], cx - bw / 2 - 2, padT + ch - h1 - 4);
+			}
+			if (!dense || (b && b[i] > 0)) {
+				ctx.fillStyle = '#00C8FF'; ctx.font = valFont;
+				ctx.textAlign = 'center';
+				ctx.fillText(b && b[i] != null ? b[i] : 0, cx + bw / 2 + 2, padT + ch - h2 - 4);
+			}
+			if (i % labelStep === 0) {
+				ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = lblFont;
+				ctx.textAlign = 'center';
+				ctx.fillText(labels[i], cx, h - 6);
+			}
+		}
+	}
+
+	// 近12小时上下线双柱:纵轴 0–4,横坐标/柱顶数值与柱组中心对齐
+	function drawHourlyDoubleBar(id, a, b, labels) {
+		var p = prepareCanvas(id); if (!p) return;
+		var ctx = p.ctx, w = p.w, h = p.h;
+		var maxV = 4;
+		var padL = 32, padR = 8, padT = 16, padB = 26;
+		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 n = a.length;
+		var gw = cw / n;
+		var bw = Math.max(3, Math.min(14, gw * 0.28));
+		var gap = 2;
+		ctx.textAlign = 'center';
+		for (var i = 0; i < n; i++) {
+			var cx = padL + (i + 0.5) * gw;
+			var v1 = Number(a[i]) || 0;
+			var v2 = Number(b && b[i] != null ? b[i] : 0) || 0;
+			var h1 = (Math.min(v1, maxV) / maxV) * ch;
+			var h2 = (Math.min(v2, maxV) / maxV) * ch;
+			var x1 = cx - gap / 2 - bw;
+			var x2 = cx + gap / 2;
+			var g1 = ctx.createLinearGradient(0, padT + ch - h1, 0, padT + ch);
+			g1.addColorStop(0, '#00FF88'); g1.addColorStop(1, 'rgba(0,255,136,0.15)');
+			ctx.fillStyle = g1;
+			ctx.fillRect(x1, padT + ch - h1, bw, h1);
+			var g2 = ctx.createLinearGradient(0, padT + ch - h2, 0, padT + ch);
+			g2.addColorStop(0, '#00C8FF'); g2.addColorStop(1, 'rgba(0,200,255,0.15)');
+			ctx.fillStyle = g2;
+			ctx.fillRect(x2, padT + ch - h2, bw, h2);
+			ctx.font = 'bold 11px Microsoft YaHei';
+			ctx.fillStyle = '#00FF88';
+			ctx.fillText(String(v1), x1 + bw / 2, padT + ch - h1 - 4);
+			ctx.fillStyle = '#00C8FF';
+			ctx.fillText(String(v2), x2 + bw / 2, padT + ch - h2 - 4);
+			ctx.fillStyle = 'rgba(77,201,245,0.95)';
+			ctx.font = '12px Microsoft YaHei';
+			ctx.fillText(labels[i] != null ? String(labels[i]) : String(i), cx, h - 6);
 		}
 	}
 
@@ -927,6 +985,7 @@
 			html += '<td class="op-name">' + r.name + '</td>';
 			html += '<td class="op-val">' + r.real + '</td>';
 			html += '<td class="op-val">' + r.shift + '</td>';
+			html += '<td class="op-val">' + (r.prevShift || 0) + '</td>';
 			html += '</tr>';
 		}
 		html = padOpTableRowsToPageSize(html, slice.length);
@@ -981,6 +1040,7 @@
 						name: p.title || '',
 						real: 0,
 						shift: 0,
+						prevShift: 0,
 						_opNum: parseOprnoNum(code)
 					});
 				}
@@ -992,6 +1052,7 @@
 				if (out.length > 0) {
 					updateHourlyCapacityByApi();
 				}
+				updateDeviceAlarmAlertsByApi();
 			}
 		});
 	}
@@ -1006,6 +1067,7 @@
 		if (!oprnos.length) return;
 		fetchCapacityInto('dayCapacityData', LINE_SN, oprnos, 'real', {});
 		fetchCapacityInto('shiftCapacityData', LINE_SN, oprnos, 'shift', { fixedTwoShift: 'true' });
+		fetchCapacityInto('prevShiftCapacityData', LINE_SN, oprnos, 'prevShift', { fixedTwoShift: 'true' });
 	}
 
 	function fetchCapacityInto(action, lineSn, oprnos, field, extraData) {
@@ -1086,6 +1148,36 @@
 		return [];
 	}
 
+	function parseDeviceSnParts(deviceSn) {
+		var s = String(deviceSn || '').trim().toUpperCase();
+		var parts = s.split('_').filter(function(p) { return !!p; });
+		var line = parts.length > 0 ? parts[0] : '';
+		var oprno = '';
+		for (var i = 0; i < parts.length; i++) {
+			if (/^OP\d+$/.test(parts[i])) {
+				oprno = parts[i];
+				break;
+			}
+		}
+		if (!oprno) {
+			var m = s.match(/OP\d+/);
+			if (m) oprno = m[0];
+		}
+		return { line: line, oprno: oprno };
+	}
+	function resolveAlertProcessName(oprno) {
+		var code = String(oprno || '').toUpperCase();
+		var num = parseOprnoNum(code);
+		var list = mockData.opTable || [];
+		for (var i = 0; i < list.length; i++) {
+			var c = String((list[i] && list[i].code) || '').toUpperCase();
+			if (!c) continue;
+			if (c === code || (num >= 0 && parseOprnoNum(c) === num)) {
+				return list[i].name || '';
+			}
+		}
+		return '';
+	}
 	function updateDeviceAlarmAlertsByApi() {
 		$.ajax({
 			url: '${ctx}/mes/mesProductScreen/alertList',
@@ -1093,17 +1185,24 @@
 			dataType: 'json',
 			data: {
 				pageNo: 1,
-				pageSize: 50,
-				state: '0' // 只显示待处理的报警
+				pageSize: 200,
+				state: '0'
 			},
 			success: function(resp) {
 				var rows = extractUserList(resp);
 				var alerts = [];
 				for (var i = 0; i < rows.length; i++) {
 					var r = rows[i] || {};
-					if (String(r.state) !== '0') continue; // 双保险:跳过非待处理
+					if (String(r.state) !== '0') continue;
+					var sn = r.deviceSn || r.device_sn || r.deviceCode || r.device_code || '';
+					var parts = parseDeviceSnParts(sn);
+					var oprno = String(r.oprno || parts.oprno || '').toUpperCase();
+					if (!oprno) continue;
+					if (!isOprnoInRange(oprno, OP_FROM, OP_TO)) continue;
 					var typ = (r.type != null && r.type !== undefined) ? String(r.type) : '';
-					alerts.push({ no: alerts.length + 1, text: typ });
+					var pname = resolveAlertProcessName(oprno);
+					var text = oprno + (pname ? (' ' + pname) : '') + (typ ? (' | ' + typ) : '');
+					alerts.push({ no: alerts.length + 1, text: text, oprno: oprno });
 				}
 				mockData.alerts = alerts;
 				renderAlerts();
@@ -1191,7 +1290,7 @@
 		var q = mockData.qualityInspect;
 		if (q) drawDoubleBar('chartQuality', q.produced, q.defect, q.labels, q.max);
 		var hr = mockData.hourly;
-		if (hr) drawAreaLine('chartHourly', hr.up, hr.down, hr.labels, hr.max);
+		if (hr) drawHourlyDoubleBar('chartHourly', hr.up, hr.down, hr.labels);
 	}
 
 	function refreshData() {

+ 162 - 45
src/main/resources/views/modules/mes/mesScreen6.html

@@ -300,7 +300,7 @@
 	<div class="mes4-panel p-alert">
 		<div class="mes4-ptitle">  产线阶段报警信息  </div>
 		<div class="mes4-pbody" style="padding:0">
-			<div class="alert-head"><div>序号</div><div>报警类型</div></div>
+			<div class="alert-head"><div>序号</div><div>工位 / 报警类型</div></div>
 			<div id="alertList"></div>
 		</div>
 	</div>
@@ -331,6 +331,7 @@
 					<th>小时产能</th>
 					<th>实际产能</th>
 					<th>当班产能(12小时内)</th>
+					<th>上班产量</th>
 				</tr>
 				</thead>
 				<tbody id="opTableBody"></tbody>
@@ -386,7 +387,7 @@
 	</div>
 
 	<div class="mes4-panel p-hourly">
-		<div class="mes4-ptitle">  每小时上下线数据分析  </div>
+		<div class="mes4-ptitle">  近12小时上下线数据分析  </div>
 		<div class="legend-inline">
 			<span><i class="dot" style="background:#00FF88"></i>上线</span>
 			<span><i class="dot" style="background:#00C8FF"></i>下线</span>
@@ -424,7 +425,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="7">&nbsp;</td></tr>';
 		}
 		return html;
 	}
@@ -476,10 +477,11 @@
 	var mockData = {};
 	/** 每小时上下线:mesProduct/hourlyUpDownOkData(本页 lineSn=ZP,上线 OP190 / 下线 OP250) */
 	var HOURLY_API_CFG = { lineSn: 'ZP', onlineOprno: 'OP190', offlineOprno: 'OP250' };
-	function buildHourLabels24() {
+	function buildRecentHourLabels12() {
 		var labels = [];
-		for (var i = 1; i <= 24; i++) {
-			labels.push(pad(i));
+		var nowH = new Date().getHours();
+		for (var i = 11; i >= 0; i--) {
+			labels.push(String((nowH - i + 24) % 24));
 		}
 		return labels;
 	}
@@ -491,19 +493,20 @@
 			data: {
 				lineSn: HOURLY_API_CFG.lineSn,
 				onlineOprno: HOURLY_API_CFG.onlineOprno,
-				offlineOprno: HOURLY_API_CFG.offlineOprno
+				offlineOprno: HOURLY_API_CFG.offlineOprno,
+				recentHours: 12
 			},
 			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) {
+					if (!d.up || !d.down || d.up.length < 1) {
 						return;
 					}
 					mockData.hourly = {
-						labels: buildHourLabels24(),
+						labels: d.labels && d.labels.length ? d.labels : buildRecentHourLabels12(),
 						up: d.up,
 						down: d.down,
-						max: typeof d.max === 'number' ? d.max : 100
+						max: 4
 					};
 					drawAllCharts();
 				}
@@ -525,18 +528,18 @@
 
 		// 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 }
+			{ code: 'OP10', name: '上料+打码',       plan: 50, hour: 50, real: 50, shift: 50, prevShift: 50 },
+			{ code: 'OP20', name: '底板组件-正面FSW', plan: 50, hour: 50, real: 50, shift: 50, prevShift: 50 },
+			{ code: 'OP25', name: '底板组件-打磨',    plan: 50, hour: 50, real: 50, shift: 50, prevShift: 50 },
+			{ code: 'OP30', name: '底板组件-反面FSW', plan: 50, hour: 50, real: 50, shift: 50, prevShift: 50 },
+			{ code: 'OP40', name: '底板组件-CNC',     plan: 50, hour: 50, real: 50, shift: 50, prevShift: 50 },
+			{ code: 'OP50', name: '干冰清洁',         plan: 50, hour: 50, real: 50, shift: 50, prevShift: 50 },
+			{ code: 'OP55', name: '模组梁+封板预装',  plan: 50, hour: 50, real: 50, shift: 50, prevShift: 50 },
+			{ code: 'OP60', name: '封板正面FSW',      plan: 50, hour: 50, real: 50, shift: 50, prevShift: 50 },
+			{ code: 'OP65', name: '打磨',            plan: 50, hour: 50, real: 50, shift: 50, prevShift: 50 },
+			{ code: 'OP70', name: '封板反面FSW',      plan: 50, hour: 50, real: 50, shift: 50, prevShift: 50 },
+			{ code: 'OP75', name: '下线检测',         plan: 50, hour: 50, real: 50, shift: 50, prevShift: 50 },
+			{ code: 'OP80', name: '装箱',             plan: 50, hour: 50, real: 50, shift: 50, prevShift: 50 }
 		];
 
 		// 数据卡
@@ -571,14 +574,13 @@
 			max: 1300, min: 1000
 		};
 
-		// 每小时上下线 (24 小时)
-		var hourLabels = [], up = [], down = [];
-		for (var i = 1; i <= 24; i++) {
-			hourLabels.push(pad(i));
-			up.push(Math.round(60 + Math.random() * 30));
-			down.push(Math.round(50 + Math.random() * 30));
+		// 近12小时上下线
+		var hourLabels = buildRecentHourLabels12(), up = [], down = [];
+		for (var i = 0; i < 12; i++) {
+			up.push(Math.round(Math.random() * 4));
+			down.push(Math.round(Math.random() * 4));
 		}
-		mockData.hourly = { labels: hourLabels, up: up, down: down, max: 100 };
+		mockData.hourly = { labels: hourLabels, up: up, down: down, max: 4 };
 
 		// 各月产量
 		mockData.monthly = {
@@ -621,7 +623,7 @@
 		}
 	}
 
-	// 双柱状图 - 近七日产品质检 (产出/不良)
+	// 双柱状图 - 近七日产品质检 (产出/不良) / 每小时上下线
 	function drawDoubleBar(id, a, b, labels, maxV) {
 		var p = prepareCanvas(id); if (!p) return;
 		var ctx = p.ctx, w = p.w, h = p.h;
@@ -630,24 +632,80 @@
 		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;
-		for (var i = 0; i < a.length; i++) {
+		var n = a.length;
+		var dense = n > 12;
+		var gw = cw / n, bw = gw * (dense ? 0.32 : 0.3);
+		var labelStep = dense ? 2 : 1;
+		var valFont = dense ? 'bold 9px Microsoft YaHei' : 'bold 10px Microsoft YaHei';
+		var lblFont = dense ? '10px Microsoft YaHei' : '11px Microsoft YaHei';
+		for (var i = 0; i < n; i++) {
 			var cx = padL + i * gw + gw / 2;
 			var h1 = (a[i] / maxV) * ch;
 			var g1 = ctx.createLinearGradient(0, padT + ch - h1, 0, padT + ch);
 			g1.addColorStop(0, '#00FF88'); g1.addColorStop(1, 'rgba(0,255,136,0.15)');
 			ctx.fillStyle = g1;
 			ctx.fillRect(cx - bw - 2, padT + ch - h1, bw, h1);
-			var h2 = (b[i] / maxV) * ch;
+			var h2 = ((b && b[i] != null ? b[i] : 0) / maxV) * ch;
 			var g2 = ctx.createLinearGradient(0, padT + ch - h2, 0, padT + ch);
 			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.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);
+			if (!dense || a[i] > 0) {
+				ctx.fillStyle = '#00FF88'; ctx.font = valFont;
+				ctx.textAlign = 'center'; ctx.fillText(a[i], cx - bw / 2 - 2, padT + ch - h1 - 4);
+			}
+			if (!dense || (b && b[i] > 0)) {
+				ctx.fillStyle = '#00C8FF'; ctx.font = valFont;
+				ctx.textAlign = 'center';
+				ctx.fillText(b && b[i] != null ? b[i] : 0, cx + bw / 2 + 2, padT + ch - h2 - 4);
+			}
+			if (i % labelStep === 0) {
+				ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = lblFont;
+				ctx.textAlign = 'center';
+				ctx.fillText(labels[i], cx, h - 6);
+			}
+		}
+	}
+
+	// 近12小时上下线双柱:纵轴 0–4,横坐标/柱顶数值与柱组中心对齐
+	function drawHourlyDoubleBar(id, a, b, labels) {
+		var p = prepareCanvas(id); if (!p) return;
+		var ctx = p.ctx, w = p.w, h = p.h;
+		var maxV = 4;
+		var padL = 32, padR = 8, padT = 16, padB = 26;
+		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 n = a.length;
+		var gw = cw / n;
+		var bw = Math.max(3, Math.min(14, gw * 0.28));
+		var gap = 2;
+		ctx.textAlign = 'center';
+		for (var i = 0; i < n; i++) {
+			var cx = padL + (i + 0.5) * gw;
+			var v1 = Number(a[i]) || 0;
+			var v2 = Number(b && b[i] != null ? b[i] : 0) || 0;
+			var h1 = (Math.min(v1, maxV) / maxV) * ch;
+			var h2 = (Math.min(v2, maxV) / maxV) * ch;
+			var x1 = cx - gap / 2 - bw;
+			var x2 = cx + gap / 2;
+			var g1 = ctx.createLinearGradient(0, padT + ch - h1, 0, padT + ch);
+			g1.addColorStop(0, '#00FF88'); g1.addColorStop(1, 'rgba(0,255,136,0.15)');
+			ctx.fillStyle = g1;
+			ctx.fillRect(x1, padT + ch - h1, bw, h1);
+			var g2 = ctx.createLinearGradient(0, padT + ch - h2, 0, padT + ch);
+			g2.addColorStop(0, '#00C8FF'); g2.addColorStop(1, 'rgba(0,200,255,0.15)');
+			ctx.fillStyle = g2;
+			ctx.fillRect(x2, padT + ch - h2, bw, h2);
+			ctx.font = 'bold 11px Microsoft YaHei';
+			ctx.fillStyle = '#00FF88';
+			ctx.fillText(String(v1), x1 + bw / 2, padT + ch - h1 - 4);
+			ctx.fillStyle = '#00C8FF';
+			ctx.fillText(String(v2), x2 + bw / 2, padT + ch - h2 - 4);
+			ctx.fillStyle = 'rgba(77,201,245,0.95)';
+			ctx.font = '12px Microsoft YaHei';
+			ctx.fillText(labels[i] != null ? String(labels[i]) : String(i), cx, h - 6);
 		}
 	}
 
@@ -927,6 +985,7 @@
 			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 += '<td class="op-val">' + (r.prevShift || 0) + '</td>';
 			html += '</tr>';
 		}
 		html = padOpTableRowsToPageSize(html, slice.length);
@@ -1004,7 +1063,8 @@
 						plan: (mockData.cards && mockData.cards.planProduction) || 0,
 						hour: 0,
 						real: 0,
-						shift: 0
+						shift: 0,
+						prevShift: 0
 					});
 				}
 				if (out.length > 0) {
@@ -1013,6 +1073,7 @@
 					renderTable();
 					startOpTablePageRotate();
 					updateHourlyCapacityByApi();
+					updateDeviceAlarmAlertsByApi();
 				}
 			}
 		});
@@ -1035,9 +1096,10 @@
 		fetchCapacityInto('hourlyCapacityData', 'ZP', oprnos, 'hour');
 		fetchCapacityInto('dayCapacityData', 'ZP', oprnos, 'real');
 		fetchCapacityInto('shiftCapacityData', 'ZP', oprnos, 'shift');
+		fetchCapacityInto('prevShiftCapacityData', 'ZP', oprnos, 'prevShift');
 	}
 
-	// 通用:拉取产能数据并写入表格指定字段(hour=小时产能, real=实际/当天产能, shift=当班产能)
+	// 通用:拉取产能数据并写入表格指定字段(hour=小时产能, real=实际/当天产能, shift=当班产能, prevShift=上班产量)
 	function fetchCapacityInto(action, lineSn, oprnos, field) {
 		$.ajax({
 			url: '${ctx}/mes/mesProductRecord/' + action,
@@ -1179,6 +1241,54 @@
 		return [];
 	}
 
+	function parseDeviceSnParts(deviceSn) {
+		var s = String(deviceSn || '').trim().toUpperCase();
+		var parts = s.split('_').filter(function(p) { return !!p; });
+		var line = parts.length > 0 ? parts[0] : '';
+		var oprno = '';
+		for (var i = 0; i < parts.length; i++) {
+			if (/^OP\d+$/.test(parts[i])) {
+				oprno = parts[i];
+				break;
+			}
+		}
+		if (!oprno) {
+			var m = s.match(/OP\d+/);
+			if (m) oprno = m[0];
+		}
+		return { line: line, oprno: oprno };
+	}
+	function parseOprnoNum(code) {
+		var m = String(code || '').toUpperCase().match(/OP(\d+)/);
+		return m ? parseInt(m[1], 10) : -1;
+	}
+	function isAlertOprnoOnZpLine(oprno) {
+		var num = parseOprnoNum(oprno);
+		var list = mockData.opTable || [];
+		if (!list.length) {
+			return false;
+		}
+		for (var i = 0; i < list.length; i++) {
+			var c = String((list[i] && list[i].code) || '').toUpperCase();
+			if (!c) continue;
+			if (parseOprnoNum(c) === num && num >= 0) {
+				return true;
+			}
+		}
+		return false;
+	}
+	function resolveAlertProcessName(oprno) {
+		var num = parseOprnoNum(oprno);
+		var list = mockData.opTable || [];
+		for (var i = 0; i < list.length; i++) {
+			var c = String((list[i] && list[i].code) || '').toUpperCase();
+			if (!c) continue;
+			if (parseOprnoNum(c) === num && num >= 0) {
+				return list[i].name || '';
+			}
+		}
+		return '';
+	}
 	function updateDeviceAlarmAlertsByApi() {
 		$.ajax({
 			url: '${ctx}/mes/mesProductScreen/alertList',
@@ -1186,17 +1296,24 @@
 			dataType: 'json',
 			data: {
 				pageNo: 1,
-				pageSize: 50,
-				state: '0' // 只显示待处理的报警
+				pageSize: 200,
+				state: '0'
 			},
 			success: function(resp) {
 				var rows = extractUserList(resp);
 				var alerts = [];
 				for (var i = 0; i < rows.length; i++) {
 					var r = rows[i] || {};
-					if (String(r.state) !== '0') continue; // 双保险:跳过非待处理
+					if (String(r.state) !== '0') continue;
+					var sn = r.deviceSn || r.device_sn || r.deviceCode || r.device_code || '';
+					var parts = parseDeviceSnParts(sn);
+					var oprno = String(r.oprno || parts.oprno || '').toUpperCase();
+					if (!oprno) continue;
+					if (!isAlertOprnoOnZpLine(oprno)) continue;
 					var typ = (r.type != null && r.type !== undefined) ? String(r.type) : '';
-					alerts.push({ no: alerts.length + 1, text: typ });
+					var pname = resolveAlertProcessName(oprno);
+					var text = oprno + (pname ? (' ' + pname) : '') + (typ ? (' | ' + typ) : '');
+					alerts.push({ no: alerts.length + 1, text: text, oprno: oprno });
 				}
 				mockData.alerts = alerts;
 				renderAlerts();
@@ -1345,7 +1462,7 @@
 		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);
+		if (hr) drawHourlyDoubleBar('chartHourly', hr.up, hr.down, hr.labels);
 		var mo = mockData.monthly;
 		if (mo) drawMonthBar('chartMonth', mo.data, mo.labels, mo.max);
 		drawGauge('gaugeRate', mockData.rate || 0);