Explorar o código

涂胶时间、ERP优化、模块显示优化

jingbo hai 3 meses
pai
achega
c12e74d713
Modificáronse 56 ficheiros con 3160 adicións e 731 borrados
  1. 32 0
      electron/dist/builder-effective-config.yaml
  2. 3 0
      src/main/java/com/jeesite/modules/mes/dao/MesDeviceDao.java
  3. 15 0
      src/main/java/com/jeesite/modules/mes/dao/MesIconsCategoryDao.java
  4. 15 0
      src/main/java/com/jeesite/modules/mes/dao/MesProductGlueDurationDao.java
  5. 36 0
      src/main/java/com/jeesite/modules/mes/entity/MesDocument.java
  6. 28 0
      src/main/java/com/jeesite/modules/mes/entity/MesIcons.java
  7. 73 0
      src/main/java/com/jeesite/modules/mes/entity/MesIconsCategory.java
  8. 37 1
      src/main/java/com/jeesite/modules/mes/entity/MesProductArcWelding.java
  9. 122 0
      src/main/java/com/jeesite/modules/mes/entity/MesProductGlueDuration.java
  10. 21 1
      src/main/java/com/jeesite/modules/mes/entity/MesProductQm.java
  11. 5 4
      src/main/java/com/jeesite/modules/mes/erp/ErpInteractionLogService.java
  12. 45 0
      src/main/java/com/jeesite/modules/mes/erp/ErpMaterialUnitOption.java
  13. 107 8
      src/main/java/com/jeesite/modules/mes/erp/ErpProductionMaterialService.java
  14. 50 51
      src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirectSaveService.java
  15. 25 25
      src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirectSubmitRequest.java
  16. 44 0
      src/main/java/com/jeesite/modules/mes/req/MesDeviceReportStateReq.java
  17. 346 0
      src/main/java/com/jeesite/modules/mes/service/MesDeviceOeeStatsService.java
  18. 4 0
      src/main/java/com/jeesite/modules/mes/service/MesDeviceService.java
  19. 59 0
      src/main/java/com/jeesite/modules/mes/service/MesDeviceStateReportService.java
  20. 145 0
      src/main/java/com/jeesite/modules/mes/service/MesDocumentPreviewService.java
  21. 68 0
      src/main/java/com/jeesite/modules/mes/service/MesIconsCategoryService.java
  22. 160 7
      src/main/java/com/jeesite/modules/mes/service/MesIconsService.java
  23. 54 0
      src/main/java/com/jeesite/modules/mes/service/MesProductGlueDurationService.java
  24. 113 8
      src/main/java/com/jeesite/modules/mes/web/ErpTransferDirectController.java
  25. 50 286
      src/main/java/com/jeesite/modules/mes/web/MesDeviceController.java
  26. 95 0
      src/main/java/com/jeesite/modules/mes/web/MesDocumentController.java
  27. 117 0
      src/main/java/com/jeesite/modules/mes/web/MesIconsCategoryController.java
  28. 22 24
      src/main/java/com/jeesite/modules/mes/web/MesIconsController.java
  29. 28 3
      src/main/java/com/jeesite/modules/mes/web/MesProductArcWeldingController.java
  30. 24 22
      src/main/java/com/jeesite/modules/mes/web/MesProductController.java
  31. 132 0
      src/main/java/com/jeesite/modules/mes/web/MesProductGlueDurationController.java
  32. 25 6
      src/main/java/com/jeesite/modules/mes/web/MesProductQmController.java
  33. 1 0
      src/main/resources/config/application.yml
  34. 9 0
      src/main/resources/mappings/modules/mes/MesDeviceDao.xml
  35. 15 0
      src/main/resources/mappings/modules/mes/MesIconsCategoryDao.xml
  36. 5 0
      src/main/resources/mappings/modules/mes/MesProductGlueDurationDao.xml
  37. 28 0
      src/main/resources/mappings/modules/mes/MesProductQmDao.xml
  38. 0 9
      src/main/resources/views/modules/mes/erpProductionOrderList.html
  39. 0 7
      src/main/resources/views/modules/mes/erpProductionOrderPushdown.html
  40. 272 104
      src/main/resources/views/modules/mes/erpTransferDirectForm.html
  41. 1 1
      src/main/resources/views/modules/mes/erpTransferDirectList.html
  42. 6 0
      src/main/resources/views/modules/mes/mesDeviceOeeScreen.html
  43. 32 1
      src/main/resources/views/modules/mes/mesDocumentList.html
  44. 98 0
      src/main/resources/views/modules/mes/mesDocumentPreview.html
  45. 78 0
      src/main/resources/views/modules/mes/mesIconsCategoryForm.html
  46. 80 0
      src/main/resources/views/modules/mes/mesIconsCategoryList.html
  47. 11 0
      src/main/resources/views/modules/mes/mesIconsForm.html
  48. 7 0
      src/main/resources/views/modules/mes/mesIconsList.html
  49. 19 15
      src/main/resources/views/modules/mes/mesIntegratedScreen.html
  50. 15 66
      src/main/resources/views/modules/mes/mesProductArcWeldingList.html
  51. 91 0
      src/main/resources/views/modules/mes/mesProductGlueDurationForm.html
  52. 85 0
      src/main/resources/views/modules/mes/mesProductGlueDurationList.html
  53. 15 69
      src/main/resources/views/modules/mes/mesProductQmList.html
  54. 97 0
      src/main/resources/views/modules/mes/mesProductQmWhiteList.html
  55. 78 0
      src/main/resources/views/modules/mes/mesProductSpotWeldingList.html
  56. 17 13
      src/main/resources/views/modules/mes/mesScreen4.html

+ 32 - 0
electron/dist/builder-effective-config.yaml

@@ -0,0 +1,32 @@
+directories:
+  output: dist
+  buildResources: build
+appId: com.jeesite.mescloud
+productName: MES Cloud
+copyright: Copyright © 2026 北京京宜安科技有限公司
+files:
+  - filter:
+      - main.js
+      - index.html
+      - preload.js
+      - icon.ico
+win:
+  target:
+    - nsis
+  icon: icon.ico
+  signAndEditExecutable: true
+mac:
+  target: dmg
+  icon: icon.icns
+linux:
+  target: AppImage
+  icon: icon.png
+nsis:
+  oneClick: false
+  allowElevation: true
+  allowToChangeInstallationDirectory: true
+  installerIcon: icon.ico
+  uninstallerIcon: icon.ico
+  installerHeaderIcon: icon.ico
+  artifactName: ${productName}-${version}.${ext}
+electronVersion: 28.3.3

+ 3 - 0
src/main/java/com/jeesite/modules/mes/dao/MesDeviceDao.java

@@ -1,5 +1,7 @@
 package com.jeesite.modules.mes.dao;
 
+import org.apache.ibatis.annotations.Param;
+
 import com.jeesite.common.dao.TreeDao;
 import com.jeesite.common.mybatis.annotation.MyBatisDao;
 import com.jeesite.modules.mes.entity.MesDevice;
@@ -14,5 +16,6 @@ public interface MesDeviceDao extends TreeDao<MesDevice> {
     public MesDevice findByInfo(MesDevice mesDevice);
     MesDevice findInfo(MesDevice mesDevice);
 
+	MesDevice findByGwAndLineSn(@Param("gw") String gw, @Param("lineSn") String lineSn);
 
 }

+ 15 - 0
src/main/java/com/jeesite/modules/mes/dao/MesIconsCategoryDao.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.MesIconsCategory;
+
+/**
+ * 功能模块分类DAO接口
+ * @author hzd
+ * @version 2026-07-09
+ */
+@MyBatisDao
+public interface MesIconsCategoryDao extends CrudDao<MesIconsCategory> {
+
+}

+ 15 - 0
src/main/java/com/jeesite/modules/mes/dao/MesProductGlueDurationDao.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.MesProductGlueDuration;
+
+/**
+ * 涂胶加工时长DAO接口
+ * @author mes
+ * @version 2026-07-13
+ */
+@MyBatisDao
+public interface MesProductGlueDurationDao extends CrudDao<MesProductGlueDuration> {
+
+}

+ 36 - 0
src/main/java/com/jeesite/modules/mes/entity/MesDocument.java

@@ -44,6 +44,10 @@ public class MesDocument extends DataEntity<MesDocument> {
 	private String remark;		// 备注
 	private Extend extend;		// 扩展字段
 	private String cateName;		// 文档名称
+	private String fileUploadId;	// 附件上传ID(列表预览用)
+	private String fileName;		// 附件文件名(列表预览用)
+	private Boolean previewable;	// 是否支持在线预览
+	private String previewUrl;		// 预览地址(列表预览用)
 
 	public MesDocument() {
 		this(null);
@@ -95,5 +99,37 @@ public class MesDocument extends DataEntity<MesDocument> {
 	public void setCateName(String cateName) {
 		this.cateName = cateName;
 	}
+
+	public String getFileUploadId() {
+		return fileUploadId;
+	}
+
+	public void setFileUploadId(String fileUploadId) {
+		this.fileUploadId = fileUploadId;
+	}
+
+	public String getFileName() {
+		return fileName;
+	}
+
+	public void setFileName(String fileName) {
+		this.fileName = fileName;
+	}
+
+	public Boolean getPreviewable() {
+		return previewable;
+	}
+
+	public void setPreviewable(Boolean previewable) {
+		this.previewable = previewable;
+	}
+
+	public String getPreviewUrl() {
+		return previewUrl;
+	}
+
+	public void setPreviewUrl(String previewUrl) {
+		this.previewUrl = previewUrl;
+	}
 	
 }

+ 28 - 0
src/main/java/com/jeesite/modules/mes/entity/MesIcons.java

@@ -4,6 +4,8 @@ import javax.validation.constraints.Size;
 
 import com.jeesite.common.entity.DataEntity;
 import com.jeesite.common.mybatis.annotation.Column;
+import com.jeesite.common.mybatis.annotation.JoinTable;
+import com.jeesite.common.mybatis.annotation.JoinTable.Type;
 import com.jeesite.common.mybatis.annotation.Table;
 import com.jeesite.common.mybatis.mapper.query.QueryType;
 
@@ -14,6 +16,7 @@ import com.jeesite.common.mybatis.mapper.query.QueryType;
  */
 @Table(name="mes_icons", alias="a", label="功能模块信息", columns={
 		@Column(name="id", attrName="id", label="id", isPK=true),
+		@Column(name="category_id", attrName="categoryId", label="所属分类", queryType=QueryType.EQ),
 		@Column(name="title", attrName="title", label="名称", queryType=QueryType.LIKE),
 		@Column(name="mode", attrName="mode", label="标识", queryType=QueryType.LIKE),
 		@Column(name="remark", attrName="remark", label="备注", queryType=QueryType.LIKE),
@@ -23,11 +26,19 @@ import com.jeesite.common.mybatis.mapper.query.QueryType;
 		@Column(name="create_date", attrName="createDate", label="create_date", 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),
+	}, joinTable={
+		@JoinTable(type=Type.LEFT_JOIN, entity=MesIconsCategory.class, attrName="this", alias="c",
+			on="c.id = a.category_id",
+			columns={
+				@Column(name="title", attrName="categoryTitle", label="分类名称"),
+			}),
 	}, orderBy="a.update_date DESC"
 )
 public class MesIcons extends DataEntity<MesIcons> {
 	
 	private static final long serialVersionUID = 1L;
+	private String categoryId;	// 所属分类
+	private String categoryTitle;	// 分类名称(关联查询)
 	private String title;		// 名称
 	private String mode;		// 标识
 	private String remark;		// 备注
@@ -40,6 +51,23 @@ public class MesIcons extends DataEntity<MesIcons> {
 	public MesIcons(String id){
 		super(id);
 	}
+
+	@Size(min=0, max=64, message="所属分类长度不能超过 64 个字符")
+	public String getCategoryId() {
+		return categoryId;
+	}
+
+	public void setCategoryId(String categoryId) {
+		this.categoryId = categoryId;
+	}
+
+	public String getCategoryTitle() {
+		return categoryTitle;
+	}
+
+	public void setCategoryTitle(String categoryTitle) {
+		this.categoryTitle = categoryTitle;
+	}
 	
 	@Size(min=0, max=100, message="名称长度不能超过 100 个字符")
 	public String getTitle() {

+ 73 - 0
src/main/java/com/jeesite/modules/mes/entity/MesIconsCategory.java

@@ -0,0 +1,73 @@
+package com.jeesite.modules.mes.entity;
+
+import javax.validation.constraints.NotBlank;
+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;
+
+/**
+ * 功能模块分类Entity
+ * @author hzd
+ * @version 2026-07-09
+ */
+@Table(name="mes_icons_category", alias="a", label="功能模块分类信息", columns={
+		@Column(name="id", attrName="id", label="id", isPK=true),
+		@Column(name="title", attrName="title", label="分类名称", queryType=QueryType.LIKE),
+		@Column(name="sort_order", attrName="sortOrder", label="排序", isQuery=false, isUpdateForce=true),
+		@Column(name="remark", attrName="remark", label="备注", queryType=QueryType.LIKE),
+		@Column(name="status", attrName="status", label="状态", comment="状态(0正常 1删除 2停用)", isUpdate=false),
+		@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="更新者", isQuery=false),
+		@Column(name="update_date", attrName="updateDate", label="更新时间", isQuery=false, isUpdateForce=true),
+	}, orderBy="a.sort_order ASC, a.update_date DESC"
+)
+public class MesIconsCategory extends DataEntity<MesIconsCategory> {
+
+	/** 默认分类「其他」固定 ID */
+	public static final String DEFAULT_ID = "1000000000000000001";
+
+	private static final long serialVersionUID = 1L;
+	private String title;		// 分类名称
+	private Integer sortOrder;	// 排序
+	private String remark;		// 备注
+
+	public MesIconsCategory() {
+		this(null);
+	}
+
+	public MesIconsCategory(String id){
+		super(id);
+	}
+
+	@NotBlank(message="分类名称不能为空")
+	@Size(min=0, max=100, message="分类名称长度不能超过 100 个字符")
+	public String getTitle() {
+		return title;
+	}
+
+	public void setTitle(String title) {
+		this.title = title;
+	}
+
+	public Integer getSortOrder() {
+		return sortOrder;
+	}
+
+	public void setSortOrder(Integer sortOrder) {
+		this.sortOrder = sortOrder;
+	}
+
+	@Size(min=0, max=255, message="备注长度不能超过 255 个字符")
+	public String getRemark() {
+		return remark;
+	}
+
+	public void setRemark(String remark) {
+		this.remark = remark;
+	}
+
+}

+ 37 - 1
src/main/java/com/jeesite/modules/mes/entity/MesProductArcWelding.java

@@ -4,7 +4,9 @@ import javax.validation.constraints.Size;
 
 import java.util.Date;
 import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.lang.StringUtils;
 import com.jeesite.common.mybatis.annotation.Column;
+import com.jeesite.common.mybatis.annotation.JoinTable;
 import com.jeesite.common.mybatis.annotation.Table;
 import com.jeesite.common.mybatis.mapper.query.QueryType;
 import com.jeesite.common.utils.excel.annotation.ExcelField;
@@ -23,6 +25,16 @@ import com.jeesite.common.utils.excel.annotation.ExcelFields;
 		@Column(name="sn", attrName="sn", label="二维码", queryType=QueryType.LIKE),
 		@Column(name="content", attrName="content", label="参数内容", isQuery=false),
 		@Column(name="create_date", attrName="createDate", label="创建时间", isPK=true),
+	},
+	joinTable={
+		@JoinTable(type=JoinTable.Type.LEFT_JOIN, entity=MesLine.class, attrName="this", alias="c",
+			on="c.sn = a.line_sn AND c.status = '0'",
+			columns={}),
+		@JoinTable(type=JoinTable.Type.LEFT_JOIN, entity=MesLineProcess.class, attrName="this", alias="b",
+			on="b.line_id = c.id AND b.status = '0' AND (b.oprno = a.oprno OR (CHAR_LENGTH(a.oprno) = 6 AND b.oprno = LEFT(a.oprno, 5)))",
+			columns={
+				@Column(name="title", attrName="oprnoTitle", label="工位名称"),
+			}),
 	}, orderBy="a.id DESC, a.create_date DESC"
 )
 public class MesProductArcWelding extends DataEntity<MesProductArcWelding> {
@@ -32,7 +44,8 @@ public class MesProductArcWelding extends DataEntity<MesProductArcWelding> {
 	private String oprno;		// 工位号
 	private String sn;		// 二维码
 	private String content;		// 参数内容
-	private String oprnoTitle;
+	private String oprnoTitle;	// 工位名称(关联工序表,支持模糊查询)
+	private String weldingCategory;	// 焊接类型:spot-点焊,arc-弧焊
 
 	@ExcelFields({
 		@ExcelField(title="id", attrName="id", align=Align.CENTER, sort=10),
@@ -94,5 +107,28 @@ public class MesProductArcWelding extends DataEntity<MesProductArcWelding> {
 	public void setOprnoTitle(String oprnoTitle) {
 		this.oprnoTitle = oprnoTitle;
 	}
+
+	public String getOprnoTitle_like() {
+		return sqlMap.getWhere().getValue("b.title", QueryType.LIKE);
+	}
+
+	public void setOprnoTitle_like(String oprnoTitle) {
+		if (StringUtils.isNotBlank(oprnoTitle)) {
+			sqlMap.getWhere().and("b.title", QueryType.LIKE, oprnoTitle.trim());
+		}
+	}
+
+	public String getWeldingCategory() {
+		return weldingCategory;
+	}
+
+	public void setWeldingCategory(String weldingCategory) {
+		this.weldingCategory = weldingCategory;
+		if ("spot".equals(weldingCategory)) {
+			sqlMap.getWhere().and("b.title", QueryType.LIKE, "点焊");
+		} else if ("arc".equals(weldingCategory)) {
+			sqlMap.getWhere().and("b.title", QueryType.LIKE, "弧焊");
+		}
+	}
 	
 }

+ 122 - 0
src/main/java/com/jeesite/modules/mes/entity/MesProductGlueDuration.java

@@ -0,0 +1,122 @@
+package com.jeesite.modules.mes.entity;
+
+import javax.validation.constraints.Size;
+
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.mybatis.annotation.Column;
+import com.jeesite.common.mybatis.annotation.JoinTable;
+import com.jeesite.common.mybatis.annotation.Table;
+import com.jeesite.common.mybatis.mapper.query.QueryType;
+
+/**
+ * 涂胶加工时长Entity
+ * @author mes
+ * @version 2026-07-13
+ */
+@Table(name="mes_product_glue_duration", alias="a", label="涂胶加工时长信息", columns={
+		@Column(name="id", attrName="id", label="编号", isPK=true),
+		@Column(name="sn", attrName="sn", label="工件码", queryType=QueryType.LIKE),
+		@Column(name="oprno", attrName="oprno", label="工位号", queryType=QueryType.LIKE),
+		@Column(name="line_sn", attrName="lineSn", label="产线编号", queryType=QueryType.LIKE),
+		@Column(name="glue_duration", attrName="glueDuration", label="涂胶时长", queryType=QueryType.LIKE),
+		@Column(name="remarks", attrName="remarks", label="备注", queryType=QueryType.LIKE),
+		@Column(name="status", attrName="status", label="状态", comment="状态(0正常 1删除 2停用)", isUpdate=false),
+		@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="更新者", isQuery=false),
+		@Column(name="update_date", attrName="updateDate", label="更新时间", isQuery=false, isUpdateForce=true),
+	},
+	joinTable={
+		@JoinTable(type=JoinTable.Type.LEFT_JOIN, entity=MesLine.class, attrName="this", alias="c",
+			on="c.sn = a.line_sn AND c.status = '0'",
+			columns={}),
+		@JoinTable(type=JoinTable.Type.LEFT_JOIN, entity=MesLineProcess.class, attrName="this", alias="b",
+			on="b.line_id = c.id AND b.status = '0' AND (b.oprno = a.oprno OR (CHAR_LENGTH(a.oprno) = 6 AND b.oprno = LEFT(a.oprno, 5)))",
+			columns={
+				@Column(name="title", attrName="oprnoTitle", label="工位名称"),
+			}),
+	}, orderBy="a.update_date DESC"
+)
+public class MesProductGlueDuration extends DataEntity<MesProductGlueDuration> {
+
+	private static final long serialVersionUID = 1L;
+	private String sn;			// 工件码
+	private String oprno;		// 工位号
+	private String lineSn;		// 产线编号
+	private String glueDuration;	// 涂胶时长
+	private String remarks;		// 备注
+	private String oprnoTitle;	// 工位名称
+
+	public MesProductGlueDuration() {
+		this(null);
+	}
+
+	public MesProductGlueDuration(String id){
+		super(id);
+	}
+
+	@Size(min=0, max=64, message="工件码长度不能超过 64 个字符")
+	public String getSn() {
+		return sn;
+	}
+
+	public void setSn(String sn) {
+		this.sn = sn;
+	}
+
+	@Size(min=0, max=32, message="工位号长度不能超过 32 个字符")
+	public String getOprno() {
+		return oprno;
+	}
+
+	public void setOprno(String oprno) {
+		this.oprno = oprno;
+	}
+
+	@Size(min=0, max=32, message="产线编号长度不能超过 32 个字符")
+	public String getLineSn() {
+		return lineSn;
+	}
+
+	public void setLineSn(String lineSn) {
+		this.lineSn = lineSn;
+	}
+
+	@Size(min=0, max=50, message="涂胶时长长度不能超过 50 个字符")
+	public String getGlueDuration() {
+		return glueDuration;
+	}
+
+	public void setGlueDuration(String glueDuration) {
+		this.glueDuration = glueDuration;
+	}
+
+	@Size(min=0, max=500, message="备注长度不能超过 500 个字符")
+	public String getRemarks() {
+		return remarks;
+	}
+
+	public void setRemarks(String remarks) {
+		this.remarks = remarks;
+	}
+
+	public String getOprnoTitle() {
+		return oprnoTitle;
+	}
+
+	public void setOprnoTitle(String oprnoTitle) {
+		this.oprnoTitle = oprnoTitle;
+	}
+
+	public String getOprnoTitle_like() {
+		return sqlMap.getWhere().getValue("b.title", QueryType.LIKE);
+	}
+
+	public void setOprnoTitle_like(String oprnoTitle) {
+		if (StringUtils.isNotBlank(oprnoTitle)) {
+			sqlMap.getWhere().and("b.title", QueryType.LIKE, oprnoTitle.trim());
+		}
+	}
+
+}

+ 21 - 1
src/main/java/com/jeesite/modules/mes/entity/MesProductQm.java

@@ -1,6 +1,7 @@
 package com.jeesite.modules.mes.entity;
 
 import java.util.Date;
+import com.jeesite.common.lang.StringUtils;
 import com.jeesite.common.mybatis.annotation.JoinTable;
 import com.jeesite.common.mybatis.annotation.JoinTable.Type;
 import com.fasterxml.jackson.annotation.JsonFormat;
@@ -35,7 +36,7 @@ import com.jeesite.common.utils.excel.annotation.ExcelFields;
 		@Column(name="test_pace", attrName="testPace", label="测试节拍"),
 		@Column(name="product_cate", attrName="productCate", label="产品类型"),
 		@Column(name="oprno", attrName="oprno", label="工位号", queryType=QueryType.LIKE),
-		@Column(name="line_sn", attrName="lineSn", label="产线编号"),
+		@Column(name="line_sn", attrName="lineSn", label="产线编号", queryType=QueryType.LIKE),
 		@Column(name="cq_duration", attrName="cqDuration", label="充气时间"),
 		@Column(name="by_duration", attrName="byDuration", label="保压时间"),
 		@Column(name="cs_duration", attrName="csDuration", label="测试时间"),
@@ -74,6 +75,7 @@ public class MesProductQm extends DataEntity<MesProductQm> {
 	private String programNo;
 	private String title;
 	private String remark;
+	private String qmCategory;	// 气密类型:whitePart-白件气密,assembly-总成气密
 
 	@ExcelFields({
 		@ExcelField(title="序号", attrName="id", align=Align.CENTER, sort=10),
@@ -311,6 +313,24 @@ public class MesProductQm extends DataEntity<MesProductQm> {
 		this.title = title;
 	}
 
+	public String getTitle_like() {
+		return sqlMap.getWhere().getValue("a.title", QueryType.LIKE);
+	}
+
+	public void setTitle_like(String title) {
+		if (StringUtils.isNotBlank(title)) {
+			sqlMap.getWhere().and("a.title", QueryType.LIKE, title.trim());
+		}
+	}
+
+	public String getQmCategory() {
+		return qmCategory;
+	}
+
+	public void setQmCategory(String qmCategory) {
+		this.qmCategory = qmCategory;
+	}
+
 	public String getRemark() {
 		return remark;
 	}

+ 5 - 4
src/main/java/com/jeesite/modules/mes/erp/ErpInteractionLogService.java

@@ -94,8 +94,8 @@ public class ErpInteractionLogService {
 		}
 		BigDecimal totalQty = request.getQty();
 		MesErpInteractionLog log = buildBase(ErpInteractionBizType.TRANSFER_DIRECT, resolveChannel(resolveRequest(httpRequest)));
-		log.setSourceBillNo(StringUtils.trim(request.getMoBillNo()));
-		log.setSourceEntryId(StringUtils.trim(request.getInstockEntryId()));
+		log.setSourceBillNo(StringUtils.trim(request.getMaterialNumber()));
+		log.setSourceEntryId(StringUtils.trim(request.getLotNumber()));
 		log.setLineCount(1);
 		log.setTotalQty(totalQty);
 		log.setQtySummary(formatQtySummary(1, totalQty));
@@ -108,8 +108,9 @@ public class ErpInteractionLogService {
 
 	private String buildTransferRequestSummary(ErpTransferDirectSubmitRequest request, BigDecimal totalQty) {
 		JSONObject json = new JSONObject(true);
-		json.put("sourceBillNo", StringUtils.trimToEmpty(request.getMoBillNo()));
-		json.put("relatedBillNo", StringUtils.trimToEmpty(request.getInstockBillNo()));
+		json.put("materialNumber", StringUtils.trimToEmpty(request.getMaterialNumber()));
+		json.put("lotNumber", StringUtils.trimToEmpty(request.getLotNumber()));
+		json.put("unitNumber", StringUtils.trimToEmpty(request.getUnitNumber()));
 		json.put("srcStockNumber", StringUtils.trimToEmpty(request.getSrcStockNumber()));
 		json.put("srcStockLocNumber", StringUtils.trimToEmpty(request.getSrcStockLocNumber()));
 		json.put("srcStockLocKey", StringUtils.trimToEmpty(request.getSrcStockLocKey()));

+ 45 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpMaterialUnitOption.java

@@ -0,0 +1,45 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+
+/**
+ * ERP 物料计量单位选项。
+ */
+public class ErpMaterialUnitOption implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private String unitNumber;
+	private String unitName;
+	private String displayLabel;
+
+	public String getUnitNumber() {
+		return unitNumber;
+	}
+
+	public void setUnitNumber(String unitNumber) {
+		this.unitNumber = unitNumber;
+	}
+
+	public String getUnitName() {
+		return unitName;
+	}
+
+	public void setUnitName(String unitName) {
+		this.unitName = unitName;
+	}
+
+	public String getDisplayLabel() {
+		if (displayLabel != null && !displayLabel.isEmpty()) {
+			return displayLabel;
+		}
+		if (unitName != null && !unitName.isEmpty()) {
+			return unitName;
+		}
+		return unitNumber == null ? "" : unitNumber;
+	}
+
+	public void setDisplayLabel(String displayLabel) {
+		this.displayLabel = displayLabel;
+	}
+}

+ 107 - 8
src/main/java/com/jeesite/modules/mes/erp/ErpProductionMaterialService.java

@@ -585,32 +585,131 @@ public class ErpProductionMaterialService {
 	}
 
 	/**
-	 * 查询物料计量单位编码(BillQuery BD_Material,PO 未带单位时回退)。
+	 * 查询物料计量单位编码(BillQuery + View 回退)。
 	 */
 	public String findMaterialUnitNumber(String materialNumber) {
+		return resolvePreferredUnitNumber(findMaterialUnitOptions(materialNumber));
+	}
+
+	/**
+	 * 查询物料可选计量单位(库存单位、基本单位等,去重)。
+	 */
+	public List<ErpMaterialUnitOption> findMaterialUnitOptions(String materialNumber) {
+		LinkedHashMap<String, ErpMaterialUnitOption> unitMap = new LinkedHashMap<>();
 		if (StringUtils.isBlank(materialNumber)) {
+			return new ArrayList<>();
+		}
+		String trimmed = materialNumber.trim();
+		collectUnitsFromBillQuery(trimmed, unitMap);
+		if (unitMap.isEmpty()) {
+			collectUnitsFromView(trimmed, unitMap);
+		}
+		return new ArrayList<>(unitMap.values());
+	}
+
+	private String resolvePreferredUnitNumber(List<ErpMaterialUnitOption> units) {
+		if (units == null || units.isEmpty()) {
 			return "";
 		}
+		return StringUtils.trimToEmpty(units.get(0).getUnitNumber());
+	}
+
+	private void collectUnitsFromBillQuery(String materialNumber, Map<String, ErpMaterialUnitOption> unitMap) {
 		JSONObject data = new JSONObject();
 		data.put("FormId", MATERIAL_FORM_ID);
-		data.put("FieldKeys", "FNumber,FUnitID.FNUMBER");
-		data.put("FilterString", "FNumber='" + escape(materialNumber.trim()) + "'");
+		data.put("FieldKeys", String.join(",",
+				"FNumber",
+				"FStoreUnitID.FNUMBER",
+				"FStoreUnitID.FNAME",
+				"FUnitID.FNUMBER",
+				"FUnitID.FNAME",
+				"FBaseUnitId.FNUMBER",
+				"FBaseUnitId.FNAME"));
+		data.put("FilterString", "FNumber='" + escape(materialNumber) + "'");
 		data.put("OrderString", "FNumber");
 		data.put("StartRow", 0);
 		data.put("Limit", 1);
 		try {
 			JSONArray rows = erpClient.billQuery(data);
 			if (rows == null || rows.isEmpty()) {
-				return "";
-			}
-			return ErpKingdeeParseUtils.readBillQueryString(rows.get(0), 1,
-					"FUnitID.FNUMBER", "FUnitId.FNUMBER", "FUnitId.FNumber");
+				return;
+			}
+			Object rowObj = rows.get(0);
+			addUnitOption(unitMap, readBillQueryUnit(rowObj, 1, "FStoreUnitID.FNUMBER", "FStoreUnitId.FNUMBER"),
+					readBillQueryUnitName(rowObj, 2, "FStoreUnitID.FNAME", "FStoreUnitId.FNAME"));
+			addUnitOption(unitMap, readBillQueryUnit(rowObj, 3, "FUnitID.FNUMBER", "FUnitId.FNUMBER"),
+					readBillQueryUnitName(rowObj, 4, "FUnitID.FNAME", "FUnitId.FNAME"));
+			addUnitOption(unitMap, readBillQueryUnit(rowObj, 5, "FBaseUnitId.FNUMBER", "FBaseUnitID.FNUMBER"),
+					readBillQueryUnitName(rowObj, 6, "FBaseUnitId.FNAME", "FBaseUnitID.FNAME"));
 		} catch (RuntimeException e) {
 			logger.warn("ERP material unit BillQuery failed, materialNumber={}: {}", materialNumber, e.getMessage());
-			return "";
 		}
 	}
 
+	private void collectUnitsFromView(String materialNumber, Map<String, ErpMaterialUnitOption> unitMap) {
+		JSONObject result = queryMaterialViewResult(materialNumber);
+		if (result == null) {
+			return;
+		}
+		addUnitOption(unitMap, firstObject(result, "FStoreUnitID", "StoreUnitID", "StoreUnitId"));
+		addUnitOption(unitMap, firstObject(result, "FUnitId", "FUnitID", "UnitId", "UnitID"));
+		addUnitOption(unitMap, firstObject(result, "FBaseUnitId", "BaseUnitId", "BaseUnitID"));
+		JSONArray materialStock = result.getJSONArray("MaterialStock");
+		if (materialStock == null) {
+			return;
+		}
+		for (int i = 0; i < materialStock.size(); i++) {
+			JSONObject stock = materialStock.getJSONObject(i);
+			if (stock == null) {
+				continue;
+			}
+			addUnitOption(unitMap, firstObject(stock, "StoreUnitID", "StoreUnitId", "FStoreUnitID"));
+		}
+	}
+
+	private JSONObject queryMaterialViewResult(String materialNumber) {
+		JSONObject data = new JSONObject();
+		data.put("CreateOrgId", 0);
+		data.put("Number", materialNumber.trim());
+		data.put("Id", "");
+		data.put("IsSortBySeq", false);
+		try {
+			return viewResult(erpClient.view(MATERIAL_FORM_ID, data));
+		} catch (RuntimeException e) {
+			logger.debug("ERP material view unit failed, material={}, msg={}", materialNumber, e.getMessage());
+			return null;
+		}
+	}
+
+	private String readBillQueryUnit(Object rowObj, int index, String... keys) {
+		return ErpKingdeeParseUtils.readBillQueryString(rowObj, index, keys);
+	}
+
+	private String readBillQueryUnitName(Object rowObj, int index, String... keys) {
+		return ErpKingdeeParseUtils.readBillQueryString(rowObj, index, keys);
+	}
+
+	private void addUnitOption(Map<String, ErpMaterialUnitOption> unitMap, JSONObject unitObj) {
+		if (unitObj == null) {
+			return;
+		}
+		addUnitOption(unitMap, getNumber(unitObj), getNameValue(unitObj));
+	}
+
+	private void addUnitOption(Map<String, ErpMaterialUnitOption> unitMap, String unitNumber, String unitName) {
+		if (StringUtils.isBlank(unitNumber)) {
+			return;
+		}
+		String number = unitNumber.trim();
+		if (unitMap.containsKey(number)) {
+			return;
+		}
+		ErpMaterialUnitOption option = new ErpMaterialUnitOption();
+		option.setUnitNumber(number);
+		option.setUnitName(StringUtils.trimToEmpty(unitName));
+		unitMap.put(number, option);
+	}
+
 	private Map<String, Boolean> queryMaterialBatchManage(Set<String> materialNumbers) {
 		Map<String, Boolean> result = new LinkedHashMap<>();
 		StringBuilder filter = new StringBuilder();

+ 50 - 51
src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirectSaveService.java

@@ -14,7 +14,7 @@ import java.util.List;
 
 /**
  * ERP 直接调拨单 Save(STK_TransferDirect)。
- * 批号来自生产入库单,便于生产订单下查关联。
+ * 外发调拨:物料、批号、数量由用户直接选择,不再关联生产订单。
  */
 @Service
 public class ErpTransferDirectSaveService {
@@ -25,22 +25,22 @@ public class ErpTransferDirectSaveService {
 
 	private final KingdeeErpClient erpClient;
 	private final KingdeeErpProperties erpProperties;
-	private final ErpPrdInStockService erpPrdInStockService;
 	private final ErpProductionMaterialService erpProductionMaterialService;
 	private final ErpStockLocService erpStockLocService;
+	private final ErpInventoryLotService erpInventoryLotService;
 	private final ErpInteractionLogService erpInteractionLogService;
 
 	public ErpTransferDirectSaveService(KingdeeErpClient erpClient,
 			KingdeeErpProperties erpProperties,
-			ErpPrdInStockService erpPrdInStockService,
 			ErpProductionMaterialService erpProductionMaterialService,
 			ErpStockLocService erpStockLocService,
+			ErpInventoryLotService erpInventoryLotService,
 			ErpInteractionLogService erpInteractionLogService) {
 		this.erpClient = erpClient;
 		this.erpProperties = erpProperties;
-		this.erpPrdInStockService = erpPrdInStockService;
 		this.erpProductionMaterialService = erpProductionMaterialService;
 		this.erpStockLocService = erpStockLocService;
+		this.erpInventoryLotService = erpInventoryLotService;
 		this.erpInteractionLogService = erpInteractionLogService;
 	}
 
@@ -51,34 +51,26 @@ public class ErpTransferDirectSaveService {
 	}
 
 	private ErpTransferDirectResult doSubmit(ErpTransferDirectSubmitRequest request) {
-		if (request == null || StringUtils.isBlank(request.getMoBillNo())) {
-			return ErpTransferDirectResult.fail("生产订单号不能为空");
+		if (request == null) {
+			return ErpTransferDirectResult.fail("请求参数不能为空");
 		}
-		if (StringUtils.isBlank(request.getInstockEntryId())) {
-			return ErpTransferDirectResult.fail("请选择生产入库行");
+		String materialNumber = StringUtils.trim(request.getMaterialNumber());
+		if (StringUtils.isBlank(materialNumber)) {
+			return ErpTransferDirectResult.fail("请选择物料编码");
+		}
+		if (!erpProperties.isAllowedTransferDirectMaterial(materialNumber)) {
+			return ErpTransferDirectResult.fail("物料 " + materialNumber + " 不在 HEV/115 调拨范围内");
+		}
+		String lotNumber = StringUtils.trim(request.getLotNumber());
+		if (StringUtils.isBlank(lotNumber)) {
+			return ErpTransferDirectResult.fail("请选择批号");
 		}
 		BigDecimal qty = ErpKingdeeParseUtils.normalizeQtyNullable(request.getQty());
 		if (qty == null || qty.compareTo(BigDecimal.ZERO) <= 0) {
 			return ErpTransferDirectResult.fail("调拨数量必须大于 0");
 		}
 
-		ErpPrdInStock instock = findInstockRow(request);
-		if (instock == null) {
-			return ErpTransferDirectResult.fail("未找到对应的生产入库分录,请刷新后重试");
-		}
-		if (!erpProperties.isAllowedTransferDirectMaterial(instock.getMaterialNumber())) {
-			return ErpTransferDirectResult.fail("物料 " + instock.getMaterialNumber() + " 不在 HEV/115 调拨范围内");
-		}
-		if (StringUtils.isBlank(instock.getLotNumber())) {
-			return ErpTransferDirectResult.fail("所选生产入库行批号为空,无法外发调拨");
-		}
-		BigDecimal instockQty = instock.getRealQty() == null ? BigDecimal.ZERO : instock.getRealQty();
-		if (qty.compareTo(instockQty) > 0) {
-			return ErpTransferDirectResult.fail("调拨数量不能超过生产入库数量 "
-					+ instockQty.stripTrailingZeros().toPlainString());
-		}
-
-		String srcStockNumber = firstNonBlank(request.getSrcStockNumber(), instock.getStockNumber(),
+		String srcStockNumber = firstNonBlank(request.getSrcStockNumber(),
 				erpProperties.getTransferDirectDefaultSrcStockNumber());
 		String destStockNumber = StringUtils.trim(request.getDestStockNumber());
 		if (StringUtils.isBlank(srcStockNumber)) {
@@ -91,6 +83,15 @@ public class ErpTransferDirectSaveService {
 			return ErpTransferDirectResult.fail("调入仓库不能与调出仓库相同");
 		}
 
+		BigDecimal lotAvailableQty = resolveLotAvailableQty(materialNumber, lotNumber, srcStockNumber);
+		if (lotAvailableQty == null) {
+			return ErpTransferDirectResult.fail("所选批号在调出仓库无可用库存,请重新选择");
+		}
+		if (qty.compareTo(lotAvailableQty) > 0) {
+			return ErpTransferDirectResult.fail("调拨数量不能超过批号可用库存 "
+					+ lotAvailableQty.stripTrailingZeros().toPlainString());
+		}
+
 		String srcStockLocNumber = StringUtils.trim(request.getSrcStockLocNumber());
 		String srcStockLocKey = StringUtils.trim(request.getSrcStockLocKey());
 		String destStockLocNumber = StringUtils.trim(request.getDestStockLocNumber());
@@ -104,13 +105,14 @@ public class ErpTransferDirectSaveService {
 			return ErpTransferDirectResult.fail("调入仓库:" + destLocMsg);
 		}
 
-		String unitNumber = resolveUnitNumber(instock);
+		String unitNumber = firstNonBlank(StringUtils.trim(request.getUnitNumber()),
+				resolvePreferredUnitNumber(erpProductionMaterialService.findMaterialUnitOptions(materialNumber)));
 		if (StringUtils.isBlank(unitNumber)) {
-			return ErpTransferDirectResult.fail("物料单位编码为空,无法调拨");
+			return ErpTransferDirectResult.fail("请选择计量单位");
 		}
 
-		JSONObject saveData = buildSaveData(instock, qty, unitNumber, srcStockNumber, srcStockLocNumber, srcStockLocKey,
-				destStockNumber, destStockLocNumber, destStockLocKey);
+		JSONObject saveData = buildSaveData(materialNumber, lotNumber, qty, unitNumber, srcStockNumber,
+				srcStockLocNumber, srcStockLocKey, destStockNumber, destStockLocNumber, destStockLocKey);
 		logger.info("ERP transfer direct save request: {}", saveData.toJSONString());
 		long erpStart = System.currentTimeMillis();
 		JSONObject response = erpClient.save(FORM_ID, saveData);
@@ -119,33 +121,23 @@ public class ErpTransferDirectSaveService {
 		return result;
 	}
 
-	private ErpPrdInStock findInstockRow(ErpTransferDirectSubmitRequest request) {
-		List<ErpPrdInStock> rows = erpPrdInStockService.findTransferSourceByMoBillNo(request.getMoBillNo().trim());
-		String entryId = request.getInstockEntryId().trim();
-		String billNo = StringUtils.trim(request.getInstockBillNo());
-		for (ErpPrdInStock row : rows) {
-			if (!entryId.equals(StringUtils.trim(row.getEntryId()))) {
-				continue;
-			}
-			if (StringUtils.isNotBlank(billNo) && !billNo.equals(StringUtils.trim(row.getBillNo()))) {
-				continue;
+	private BigDecimal resolveLotAvailableQty(String materialNumber, String lotNumber, String srcStockNumber) {
+		String stockStatus = erpProperties.getTransferDirectStockStatus();
+		String orgNumber = erpProperties.getOrgNumber();
+		List<ErpInventoryLotOption> lots = erpInventoryLotService.findAvailableLots(
+				materialNumber, srcStockNumber, stockStatus, orgNumber);
+		for (ErpInventoryLotOption lot : lots) {
+			if (lot != null && lotNumber.equals(StringUtils.trim(lot.getLotNumber()))) {
+				return lot.getQty() == null ? BigDecimal.ZERO : lot.getQty();
 			}
-			return row;
 		}
 		return null;
 	}
 
-	private String resolveUnitNumber(ErpPrdInStock instock) {
-		if (StringUtils.isNotBlank(instock.getUnitNumber())) {
-			return instock.getUnitNumber().trim();
-		}
-		return erpProductionMaterialService.findMaterialUnitNumber(instock.getMaterialNumber());
-	}
-
-	private JSONObject buildSaveData(ErpPrdInStock instock, BigDecimal qty, String unitNumber,
+	private JSONObject buildSaveData(String materialNumber, String lotNumber, BigDecimal qty, String unitNumber,
 			String srcStockNumber, String srcStockLocNumber, String srcStockLocKey,
 			String destStockNumber, String destStockLocNumber, String destStockLocKey) {
-		String orgNumber = firstNonBlank(instock.getPrdOrgNumber(), erpProperties.getOrgNumber(), "110");
+		String orgNumber = firstNonBlank(erpProperties.getOrgNumber(), "110");
 		String ownerType = erpProperties.getTransferDirectOwnerType();
 		String keeperType = erpProperties.getTransferDirectKeeperType();
 		String stockStatus = erpProperties.getTransferDirectStockStatus();
@@ -170,12 +162,12 @@ public class ErpTransferDirectSaveService {
 
 		JSONObject entry = new JSONObject(true);
 		entry.put("FEntryID", 0);
-		entry.put("FMaterialId", numberObject(instock.getMaterialNumber(), false));
+		entry.put("FMaterialId", numberObject(materialNumber, false));
 		entry.put("FUnitID", numberObject(unitNumber, false));
 		entry.put("FBaseUnitId", numberObject(unitNumber, false));
 		entry.put("FQty", erpQty);
 		entry.put("FBaseQty", erpQty);
-		entry.put("FLot", numberObject(instock.getLotNumber(), false));
+		entry.put("FLot", numberObject(lotNumber, false));
 		entry.put("FSrcStockId", numberObject(srcStockNumber, false));
 		entry.put("FDestStockId", numberObject(destStockNumber, false));
 		entry.put("FSrcStockStatusId", numberObject(stockStatus, false));
@@ -238,6 +230,13 @@ public class ErpTransferDirectSaveService {
 		return "";
 	}
 
+	private String resolvePreferredUnitNumber(List<ErpMaterialUnitOption> units) {
+		if (units == null || units.isEmpty()) {
+			return "";
+		}
+		return StringUtils.trimToEmpty(units.get(0).getUnitNumber());
+	}
+
 	private ErpTransferDirectResult parseSaveResponse(JSONObject response) {
 		if (response == null) {
 			return ErpTransferDirectResult.fail("ERP 返回为空");

+ 25 - 25
src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirectSubmitRequest.java

@@ -14,14 +14,14 @@ public class ErpTransferDirectSubmitRequest implements Serializable {
 
 	private static final long serialVersionUID = 1L;
 
-	/** 生产订单号 */
-	private String moBillNo;
-	/** 生产入库分录内码 */
-	private String instockEntryId;
-	/** 生产入库单号(校验用) */
-	private String instockBillNo;
+	/** 物料编码(HEV / 115) */
+	private String materialNumber;
+	/** 批号 */
+	private String lotNumber;
 	/** 调拨数量 */
 	private BigDecimal qty;
+	/** 计量单位编码 */
+	private String unitNumber;
 	/** 调出仓库编码 */
 	private String srcStockNumber;
 	/** 调出仓位编码 */
@@ -35,28 +35,20 @@ public class ErpTransferDirectSubmitRequest implements Serializable {
 	/** 调入仓位维度键 */
 	private String destStockLocKey;
 
-	public String getMoBillNo() {
-		return moBillNo;
+	public String getMaterialNumber() {
+		return materialNumber;
 	}
 
-	public void setMoBillNo(String moBillNo) {
-		this.moBillNo = moBillNo;
+	public void setMaterialNumber(String materialNumber) {
+		this.materialNumber = materialNumber;
 	}
 
-	public String getInstockEntryId() {
-		return instockEntryId;
+	public String getLotNumber() {
+		return lotNumber;
 	}
 
-	public void setInstockEntryId(String instockEntryId) {
-		this.instockEntryId = instockEntryId;
-	}
-
-	public String getInstockBillNo() {
-		return instockBillNo;
-	}
-
-	public void setInstockBillNo(String instockBillNo) {
-		this.instockBillNo = instockBillNo;
+	public void setLotNumber(String lotNumber) {
+		this.lotNumber = lotNumber;
 	}
 
 	public BigDecimal getQty() {
@@ -67,6 +59,14 @@ public class ErpTransferDirectSubmitRequest implements Serializable {
 		this.qty = qty;
 	}
 
+	public String getUnitNumber() {
+		return unitNumber;
+	}
+
+	public void setUnitNumber(String unitNumber) {
+		this.unitNumber = unitNumber;
+	}
+
 	public String getSrcStockNumber() {
 		return srcStockNumber;
 	}
@@ -122,10 +122,10 @@ public class ErpTransferDirectSubmitRequest implements Serializable {
 			return json;
 		}
 		ErpTransferDirectSubmitRequest req = new ErpTransferDirectSubmitRequest();
-		req.setMoBillNo(ErpPdaHttpRequestUtils.trimParameter(request, "moBillNo"));
-		req.setInstockEntryId(ErpPdaHttpRequestUtils.trimParameter(request, "instockEntryId"));
-		req.setInstockBillNo(ErpPdaHttpRequestUtils.trimParameter(request, "instockBillNo"));
+		req.setMaterialNumber(ErpPdaHttpRequestUtils.trimParameter(request, "materialNumber"));
+		req.setLotNumber(ErpPdaHttpRequestUtils.trimParameter(request, "lotNumber"));
 		req.setQty(ErpPdaHttpRequestUtils.decimalParameter(request, "qty"));
+		req.setUnitNumber(ErpPdaHttpRequestUtils.trimParameter(request, "unitNumber"));
 		req.setSrcStockNumber(ErpPdaHttpRequestUtils.trimParameter(request, "srcStockNumber"));
 		req.setSrcStockLocNumber(ErpPdaHttpRequestUtils.trimParameter(request, "srcStockLocNumber"));
 		req.setSrcStockLocKey(ErpPdaHttpRequestUtils.trimParameter(request, "srcStockLocKey"));

+ 44 - 0
src/main/java/com/jeesite/modules/mes/req/MesDeviceReportStateReq.java

@@ -0,0 +1,44 @@
+package com.jeesite.modules.mes.req;
+
+/**
+ * 工位客户端按 gw + lineSn 上报设备状态
+ */
+public class MesDeviceReportStateReq {
+
+	private String gw;
+	private String lineSn;
+	private String state;
+	private String content;
+
+	public String getGw() {
+		return gw;
+	}
+
+	public void setGw(String gw) {
+		this.gw = gw;
+	}
+
+	public String getLineSn() {
+		return lineSn;
+	}
+
+	public void setLineSn(String lineSn) {
+		this.lineSn = lineSn;
+	}
+
+	public String getState() {
+		return state;
+	}
+
+	public void setState(String state) {
+		this.state = state;
+	}
+
+	public String getContent() {
+		return content;
+	}
+
+	public void setContent(String content) {
+		this.content = content;
+	}
+}

+ 346 - 0
src/main/java/com/jeesite/modules/mes/service/MesDeviceOeeStatsService.java

@@ -0,0 +1,346 @@
+package com.jeesite.modules.mes.service;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.jeesite.common.collect.ListUtils;
+import com.jeesite.common.collect.MapUtils;
+import com.jeesite.common.lang.DateUtils;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.modules.mes.entity.MesDevice;
+import com.jeesite.modules.mes.entity.MesDeviceState;
+
+/**
+ * 设备 OEE / 开动率统计
+ */
+@Service
+public class MesDeviceOeeStatsService {
+
+	@Autowired
+	private MesDeviceService mesDeviceService;
+
+	@Autowired
+	private MesDeviceStateService mesDeviceStateService;
+
+	/**
+	 * 构建设备综合效率大屏数据
+	 */
+	public Map<String, Object> buildOeeScreenData() {
+		Map<String, Object> data = MapUtils.newHashMap();
+		Date now = new Date();
+		Calendar calendar = Calendar.getInstance();
+		calendar.set(Calendar.HOUR_OF_DAY, 0);
+		calendar.set(Calendar.MINUTE, 0);
+		calendar.set(Calendar.SECOND, 0);
+		calendar.set(Calendar.MILLISECOND, 0);
+		Date todayStart = calendar.getTime();
+		long elapsedSeconds = Math.max(1L, (now.getTime() - todayStart.getTime()) / 1000);
+
+		List<MesDevice> devices = mesDeviceService.findList(new MesDevice());
+		List<Map<String, Object>> deviceCards = ListUtils.newArrayList();
+		List<Map<String, Object>> ranking = ListUtils.newArrayList();
+		long runningCount = 0;
+		long faultCount = 0;
+		long maintenanceCount = 0;
+		long alarmCount = 0;
+		long stoppedCount = 0;
+		long offlineCount = 0;
+		long unknownCount = 0;
+		long totalRunningSeconds = 0;
+
+		for (int i = 0; i < devices.size(); i++) {
+			MesDevice device = devices.get(i);
+			MesDeviceState latestQuery = new MesDeviceState();
+			latestQuery.setDeviceSn(device.getSn());
+			MesDeviceState latestState = StringUtils.isBlank(device.getSn()) ? null : mesDeviceStateService.findInfo(latestQuery);
+			String state = latestState != null && StringUtils.isNotBlank(latestState.getState()) ? latestState.getState() : device.getWorkStatus();
+			String statusCode = getDeviceScreenStatus(device, state);
+			if ("running".equals(statusCode)) {
+				runningCount++;
+			} else if ("fault".equals(statusCode)) {
+				faultCount++;
+			} else if ("maintenance".equals(statusCode)) {
+				maintenanceCount++;
+			} else if ("alarm".equals(statusCode)) {
+				alarmCount++;
+			} else if ("stopped".equals(statusCode)) {
+				stoppedCount++;
+			} else if ("offline".equals(statusCode)) {
+				offlineCount++;
+			} else {
+				unknownCount++;
+			}
+
+			MesDeviceState stateQuery = new MesDeviceState();
+			stateQuery.setDeviceSn(device.getSn());
+			stateQuery.setStartDate_gte(todayStart);
+			stateQuery.setStartDate_lte(now);
+			List<MesDeviceState> stateList = StringUtils.isBlank(device.getSn()) ? ListUtils.newArrayList() : mesDeviceStateService.findList(stateQuery);
+			long runningSeconds = getRunningSeconds(stateList, todayStart, now);
+			if (isRunningState(state) && latestState != null && latestState.getStartDate() != null && latestState.getStartDate().before(todayStart)) {
+				runningSeconds = elapsedSeconds;
+			}
+			if (runningSeconds <= 0 && isRunningState(state)) {
+				runningSeconds = getRunningSecondsFromWorkTime(device.getWorkTime(), todayStart, now);
+			}
+			totalRunningSeconds += runningSeconds;
+			double openRate = Math.min(100D, runningSeconds * 100D / elapsedSeconds);
+
+			Map<String, Object> card = MapUtils.newHashMap();
+			card.put("name", getText(device.getTreeName(), getText(device.getSn(), "-")));
+			card.put("sn", getText(device.getSn(), "-"));
+			card.put("deviceCode", getText(device.getDeviceCode(), "-"));
+			card.put("lineSn", getText(device.getLineSn(), "-"));
+			card.put("imagePath", getFirstImagePath(device.getImagePath()));
+			card.put("statusCode", statusCode);
+			card.put("statusName", getDeviceScreenStatusName(statusCode));
+			card.put("stateName", getDeviceStateName(state));
+			card.put("openRate", Math.round(openRate * 10D) / 10D);
+			card.put("openMinutes", Math.round(runningSeconds / 60D));
+			String lastReportDate = "-";
+			if (latestState != null && latestState.getStartDate() != null) {
+				lastReportDate = DateUtils.formatDate(latestState.getStartDate(), "yyyy-MM-dd HH:mm:ss");
+			} else {
+				Date workTime = parseWorkTime(device.getWorkTime());
+				if (workTime != null) {
+					lastReportDate = DateUtils.formatDate(workTime, "yyyy-MM-dd HH:mm:ss");
+				}
+			}
+			card.put("lastReportTime", lastReportDate);
+			card.put("lastReportDate", lastReportDate);
+			card.put("reportContent", getDeviceReportContent(latestState, state, statusCode));
+			card.put("imageIndex", i % 8 + 1);
+			deviceCards.add(card);
+
+			Map<String, Object> rankItem = MapUtils.newHashMap();
+			rankItem.put("name", card.get("name"));
+			rankItem.put("sn", card.get("sn"));
+			rankItem.put("openRate", card.get("openRate"));
+			rankItem.put("openMinutes", card.get("openMinutes"));
+			ranking.add(rankItem);
+		}
+
+		ranking.sort((a, b) -> Double.compare(Double.valueOf(String.valueOf(b.get("openRate"))), Double.valueOf(String.valueOf(a.get("openRate")))));
+		if (ranking.size() > 10) {
+			ranking = ranking.subList(0, 10);
+		}
+
+		List<Map<String, Object>> statusList = ListUtils.newArrayList();
+		addStatusItem(statusList, "运行", runningCount, "#35f2c2");
+		addStatusItem(statusList, "故障", faultCount, "#ff5f6d");
+		addStatusItem(statusList, "维修", maintenanceCount, "#ffc857");
+		addStatusItem(statusList, "报警", alarmCount, "#ff8a3d");
+		addStatusItem(statusList, "停机", stoppedCount, "#6b8cff");
+		addStatusItem(statusList, "离线", offlineCount, "#7d8da8");
+		addStatusItem(statusList, "未知", unknownCount, "#49a7ff");
+
+		double avgOpenRate = devices.isEmpty() ? 0D : totalRunningSeconds * 100D / (elapsedSeconds * devices.size());
+		long otherCount = Math.max(0L, devices.size() - runningCount - stoppedCount);
+		double runningRate = devices.isEmpty() ? 0D : runningCount * 100D / devices.size();
+
+		data.put("deviceTotal", devices.size());
+		data.put("runningCount", runningCount);
+		data.put("runningDevices", runningCount);
+		data.put("stoppedCount", stoppedCount);
+		data.put("stoppedDevices", stoppedCount);
+		data.put("faultCount", faultCount);
+		data.put("maintenanceCount", maintenanceCount);
+		data.put("alarmCount", alarmCount);
+		data.put("offlineCount", offlineCount);
+		data.put("unknownCount", unknownCount);
+		data.put("otherDeviceCount", otherCount);
+		data.put("avgOpenRate", Math.round(avgOpenRate * 10D) / 10D);
+		data.put("runningRate", Math.round(runningRate * 10D) / 10D);
+		data.put("statusList", statusList);
+		data.put("ranking", ranking);
+		data.put("devices", deviceCards);
+		data.put("statDate", DateUtils.formatDate(now, "yyyy-MM-dd"));
+		data.put("statTime", DateUtils.formatDate(now, "HH:mm:ss"));
+		data.put("dataNote", "当前系统缺少计划产量、良品数、理想节拍,页面展示的是基于设备状态记录计算的开动率。");
+		return data;
+	}
+
+	private long getRunningSeconds(List<MesDeviceState> stateList, Date todayStart, Date now) {
+		long runningSeconds = 0L;
+		for (MesDeviceState state : stateList) {
+			if (!isRunningState(state.getState()) || state.getStartDate() == null) {
+				continue;
+			}
+			Date start = state.getStartDate().before(todayStart) ? todayStart : state.getStartDate();
+			Date end = state.getEndDate();
+			if (end == null && state.getInterval() != null) {
+				end = new Date(state.getStartDate().getTime() + state.getInterval() * 1000L);
+			}
+			if (end == null || end.after(now)) {
+				end = now;
+			}
+			if (end.after(start)) {
+				runningSeconds += (end.getTime() - start.getTime()) / 1000;
+			}
+		}
+		return runningSeconds;
+	}
+
+	private long getRunningSecondsFromWorkTime(String workTime, Date todayStart, Date now) {
+		if (StringUtils.isBlank(workTime)) {
+			return 0L;
+		}
+		Date start = parseWorkTime(workTime);
+		if (start == null) {
+			return 0L;
+		}
+		if (start.before(todayStart)) {
+			start = todayStart;
+		}
+		if (!now.after(start)) {
+			return 0L;
+		}
+		return (now.getTime() - start.getTime()) / 1000;
+	}
+
+	private Date parseWorkTime(String workTime) {
+		String[] patterns = {"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm"};
+		for (String pattern : patterns) {
+			try {
+				return new SimpleDateFormat(pattern).parse(workTime);
+			} catch (Exception ignored) {
+			}
+		}
+		return null;
+	}
+
+	private boolean isRunningState(String state) {
+		return "1".equals(state) || "2".equals(state);
+	}
+
+	private String getDeviceScreenStatus(MesDevice device, String state) {
+		if (device.getOnline() != null && device.getOnline() == 1) {
+			return "offline";
+		}
+		if ("3".equals(state)) {
+			return "fault";
+		}
+		if ("4".equals(state)) {
+			return "maintenance";
+		}
+		if ("7".equals(state)) {
+			return "stopped";
+		}
+		if ("5".equals(state) || "6".equals(state)) {
+			return "alarm";
+		}
+		if (isRunningState(state)) {
+			return "running";
+		}
+		return "unknown";
+	}
+
+	private String getDeviceScreenStatusName(String statusCode) {
+		if ("running".equals(statusCode)) {
+			return "运行";
+		}
+		if ("fault".equals(statusCode)) {
+			return "故障";
+		}
+		if ("maintenance".equals(statusCode)) {
+			return "维修";
+		}
+		if ("stopped".equals(statusCode)) {
+			return "停机";
+		}
+		if ("alarm".equals(statusCode)) {
+			return "报警";
+		}
+		if ("offline".equals(statusCode)) {
+			return "离线";
+		}
+		return "未知";
+	}
+
+	private String getDeviceReportContent(MesDeviceState latestState, String state, String statusCode) {
+		if (latestState != null && StringUtils.isNotBlank(latestState.getContent())) {
+			return latestState.getContent().trim();
+		}
+		if ("offline".equals(statusCode)) {
+			return "设备离线";
+		}
+		if ("1".equals(state)) {
+			return "开机运行正常";
+		}
+		if ("2".equals(state)) {
+			return "手动运行中";
+		}
+		if ("3".equals(state)) {
+			return "设备故障";
+		}
+		if ("4".equals(state)) {
+			return "设备维修中";
+		}
+		if ("7".equals(state)) {
+			return "工位停机";
+		}
+		if ("5".equals(state)) {
+			return "质量报警";
+		}
+		if ("6".equals(state)) {
+			return "物流堵料";
+		}
+		return "暂无上报信息";
+	}
+
+	private String getDeviceStateName(String state) {
+		if ("1".equals(state)) {
+			return "正常";
+		}
+		if ("2".equals(state)) {
+			return "手动";
+		}
+		if ("3".equals(state)) {
+			return "故障";
+		}
+		if ("4".equals(state)) {
+			return "维修";
+		}
+		if ("7".equals(state)) {
+			return "停机";
+		}
+		if ("5".equals(state)) {
+			return "质量报警";
+		}
+		if ("6".equals(state)) {
+			return "物流堵料";
+		}
+		return "未知";
+	}
+
+	private void addStatusItem(List<Map<String, Object>> list, String name, long value, String color) {
+		Map<String, Object> item = MapUtils.newHashMap();
+		item.put("name", name);
+		item.put("value", value);
+		item.put("color", color);
+		list.add(item);
+	}
+
+	private String getText(String value, String defaultValue) {
+		return StringUtils.isBlank(value) ? defaultValue : value;
+	}
+
+	private String getFirstImagePath(String imagePath) {
+		if (StringUtils.isBlank(imagePath)) {
+			return "";
+		}
+		String[] paths = imagePath.split("\\|");
+		for (String path : paths) {
+			if (StringUtils.isNotBlank(path)) {
+				return path;
+			}
+		}
+		return "";
+	}
+}

+ 4 - 0
src/main/java/com/jeesite/modules/mes/service/MesDeviceService.java

@@ -159,4 +159,8 @@ public class MesDeviceService extends TreeService<MesDeviceDao, MesDevice> {
 	public MesDevice findByInfo(MesDevice mesDevice) {
 		return dao.findByInfo(mesDevice);
 	}
+
+	public MesDevice findByGwAndLineSn(String gw, String lineSn) {
+		return mesDeviceDao.findByGwAndLineSn(gw, lineSn);
+	}
 }

+ 59 - 0
src/main/java/com/jeesite/modules/mes/service/MesDeviceStateReportService.java

@@ -0,0 +1,59 @@
+package com.jeesite.modules.mes.service;
+
+import java.util.Date;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.jeesite.common.lang.DateUtils;
+import com.jeesite.common.lang.ObjectUtils;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.service.ServiceException;
+import com.jeesite.modules.mes.entity.MesDevice;
+import com.jeesite.modules.mes.entity.MesDeviceState;
+
+/**
+ * 工位客户端设备状态上报
+ */
+@Service
+public class MesDeviceStateReportService {
+
+	@Autowired
+	private MesDeviceService mesDeviceService;
+
+	@Autowired
+	private MesDeviceStateService mesDeviceStateService;
+
+	/**
+	 * 按工位与产线编号上报设备状态
+	 */
+	@Transactional
+	public MesDevice reportByGw(String gw, String lineSn, String state, String content) {
+		if (StringUtils.isBlank(gw) || StringUtils.isBlank(lineSn)) {
+			throw new ServiceException("工位或产线编号为空");
+		}
+		if (StringUtils.isBlank(state)) {
+			throw new ServiceException("设备状态为空");
+		}
+		MesDevice device = mesDeviceService.findByGwAndLineSn(gw, lineSn);
+		if (ObjectUtils.isEmpty(device) || StringUtils.isBlank(device.getSn())) {
+			throw new ServiceException("未找到对应设备,请检查工位与产线配置");
+		}
+
+		Date now = new Date();
+		MesDeviceState mesDeviceState = new MesDeviceState();
+		mesDeviceState.setDeviceSn(device.getSn());
+		mesDeviceState.setCode("");
+		mesDeviceState.setState(state.trim());
+		mesDeviceState.setContent(StringUtils.defaultString(content));
+		mesDeviceState.setStartDate(now);
+		mesDeviceState.setProductCate(lineSn);
+		mesDeviceStateService.updateState(mesDeviceState);
+
+		device.setWorkStatus(String.valueOf(Integer.parseInt(state.trim())));
+		device.setWorkTime(DateUtils.formatDate(now, "yyyy-MM-dd HH:mm:ss"));
+		mesDeviceService.update(device);
+		return device;
+	}
+}

+ 145 - 0
src/main/java/com/jeesite/modules/mes/service/MesDocumentPreviewService.java

@@ -0,0 +1,145 @@
+package com.jeesite.modules.mes.service;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.stereotype.Service;
+
+import com.jeesite.common.codec.EncodeUtils;
+import com.jeesite.common.config.Global;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.service.ServiceException;
+import com.jeesite.modules.file.entity.FileEntity;
+import com.jeesite.modules.file.entity.FileUpload;
+
+/**
+ * 文档在线预览服务(PDF 直出,docx/xlsx 前端解析,无需 LibreOffice)
+ */
+@Service
+public class MesDocumentPreviewService {
+
+	public static final String TYPE_PDF = "pdf";
+	public static final String TYPE_DOCX = "docx";
+	public static final String TYPE_XLSX = "xlsx";
+	public static final String TYPE_UNSUPPORTED = "unsupported";
+
+	private static final Set<String> PREVIEW_SUFFIXES = new HashSet<>(Arrays.asList(
+			".pdf", ".docx", ".xlsx"
+	));
+
+	/**
+	 * 输出原始附件流(供 PDF 内嵌或前端拉取解析)
+	 */
+	public void writeFileStream(FileUpload fileUpload, HttpServletResponse response) throws IOException {
+		if (fileUpload == null || fileUpload.getFileEntity() == null) {
+			throw new ServiceException("附件不存在或已被删除");
+		}
+		File sourceFile = resolveSourceFile(fileUpload);
+		if (sourceFile == null || !sourceFile.exists()) {
+			throw new ServiceException("附件文件不存在,请检查文件存储路径");
+		}
+		String fileName = StringUtils.defaultString(fileUpload.getFileName(), sourceFile.getName());
+		String previewType = getPreviewType(fileName);
+		if (TYPE_UNSUPPORTED.equals(previewType)) {
+			throw new ServiceException("格式不支持预览,请下载");
+		}
+		response.setContentType(getContentType(previewType));
+		if (TYPE_PDF.equals(previewType)) {
+			response.addHeader("Content-Disposition", "inline; filename*=utf-8'zh_cn'"
+					+ EncodeUtils.encodeUrl(fileName));
+		} else {
+			response.addHeader("Content-Disposition", "attachment; filename*=utf-8'zh_cn'"
+					+ EncodeUtils.encodeUrl(fileName));
+		}
+		try (InputStream in = Files.newInputStream(sourceFile.toPath());
+				OutputStream out = response.getOutputStream()) {
+			byte[] buffer = new byte[8192];
+			int len;
+			while ((len = in.read(buffer)) != -1) {
+				out.write(buffer, 0, len);
+			}
+			out.flush();
+		}
+	}
+
+	/**
+	 * 判断附件是否支持在线预览(仅 PDF、docx、xlsx)
+	 */
+	public boolean isPreviewable(String fileName) {
+		return !TYPE_UNSUPPORTED.equals(getPreviewType(fileName));
+	}
+
+	/**
+	 * 获取预览类型:pdf / docx / xlsx / unsupported
+	 */
+	public String getPreviewType(String fileName) {
+		if (StringUtils.isBlank(fileName)) {
+			return TYPE_UNSUPPORTED;
+		}
+		String lowerName = fileName.toLowerCase();
+		if (lowerName.endsWith(".pdf")) {
+			return TYPE_PDF;
+		}
+		if (lowerName.endsWith(".docx")) {
+			return TYPE_DOCX;
+		}
+		if (lowerName.endsWith(".xlsx")) {
+			return TYPE_XLSX;
+		}
+		return TYPE_UNSUPPORTED;
+	}
+
+	private String getContentType(String previewType) {
+		switch (previewType) {
+			case TYPE_PDF:
+				return "application/pdf";
+			case TYPE_DOCX:
+				return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+			case TYPE_XLSX:
+				return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+			default:
+				return "application/octet-stream";
+		}
+	}
+
+	private File resolveSourceFile(FileUpload fileUpload) {
+		FileEntity fileEntity = fileUpload.getFileEntity();
+		if (fileEntity == null) {
+			return null;
+		}
+		String realPath = fileEntity.getFileRealPath();
+		if (StringUtils.isNotBlank(realPath)) {
+			return new File(realPath);
+		}
+		String filePath = fileEntity.getFilePath();
+		if (StringUtils.isBlank(filePath)) {
+			filePath = fileUpload.getFileUrl();
+		}
+		if (StringUtils.isBlank(filePath)) {
+			return null;
+		}
+		String baseDir = Global.getConfig("file.baseDir");
+		if (StringUtils.isBlank(baseDir)) {
+			baseDir = Global.getUserfilesBaseDir(null);
+		}
+		String relativePath = filePath;
+		int userfilesIndex = relativePath.indexOf("/userfiles/");
+		if (userfilesIndex >= 0) {
+			relativePath = relativePath.substring(userfilesIndex + "/userfiles/".length());
+		} else if (StringUtils.startsWith(relativePath, "userfiles/")) {
+			relativePath = relativePath.substring("userfiles/".length());
+		} else if (StringUtils.startsWith(relativePath, "/")) {
+			relativePath = relativePath.substring(1);
+		}
+		return new File(baseDir, "userfiles/" + relativePath);
+	}
+
+}

+ 68 - 0
src/main/java/com/jeesite/modules/mes/service/MesIconsCategoryService.java

@@ -0,0 +1,68 @@
+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.cache.CacheUtils;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.common.service.ServiceException;
+import com.jeesite.modules.mes.dao.MesIconsCategoryDao;
+import com.jeesite.modules.mes.entity.MesIconsCategory;
+
+/**
+ * 功能模块分类Service
+ * @author hzd
+ * @version 2026-07-09
+ */
+@Service
+public class MesIconsCategoryService extends CrudService<MesIconsCategoryDao, MesIconsCategory> {
+
+	@Override
+	public MesIconsCategory get(MesIconsCategory mesIconsCategory) {
+		return super.get(mesIconsCategory);
+	}
+
+	@Override
+	public Page<MesIconsCategory> findPage(MesIconsCategory mesIconsCategory) {
+		return super.findPage(mesIconsCategory);
+	}
+
+	@Override
+	public List<MesIconsCategory> findList(MesIconsCategory mesIconsCategory) {
+		return super.findList(mesIconsCategory);
+	}
+
+	@Override
+	@Transactional
+	public void save(MesIconsCategory mesIconsCategory) {
+		if (mesIconsCategory.getSortOrder() == null) {
+			mesIconsCategory.setSortOrder(0);
+		}
+		super.save(mesIconsCategory);
+		CacheUtils.put("mes_icons", "icons", "");
+	}
+
+	@Override
+	@Transactional
+	public void updateStatus(MesIconsCategory mesIconsCategory) {
+		if (MesIconsCategory.DEFAULT_ID.equals(mesIconsCategory.getId())) {
+			throw new ServiceException(text("默认分类「其他」不允许停用"));
+		}
+		super.updateStatus(mesIconsCategory);
+		CacheUtils.put("mes_icons", "icons", "");
+	}
+
+	@Override
+	@Transactional
+	public void delete(MesIconsCategory mesIconsCategory) {
+		if (MesIconsCategory.DEFAULT_ID.equals(mesIconsCategory.getId())) {
+			throw new ServiceException(text("默认分类「其他」不允许删除"));
+		}
+		super.delete(mesIconsCategory);
+		CacheUtils.put("mes_icons", "icons", "");
+	}
+
+}

+ 160 - 7
src/main/java/com/jeesite/modules/mes/service/MesIconsService.java

@@ -1,19 +1,32 @@
 package com.jeesite.modules.mes.service;
 
-import java.util.ArrayList;
+import java.text.Collator;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
+import java.util.Map;
 
-import com.alibaba.fastjson.JSON;
-import com.jeesite.common.cache.CacheUtils;
-import com.jeesite.common.collect.ListUtils;
+import javax.annotation.Resource;
+
+import org.apache.shiro.subject.Subject;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import com.jeesite.common.cache.CacheUtils;
+import com.jeesite.common.collect.ListUtils;
+import com.jeesite.common.collect.MapUtils;
+import com.jeesite.common.config.Global;
 import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.StringUtils;
 import com.jeesite.common.service.CrudService;
-import com.jeesite.modules.mes.entity.MesIcons;
-import com.jeesite.modules.mes.dao.MesIconsDao;
+import com.jeesite.modules.file.entity.FileUpload;
+import com.jeesite.modules.file.service.FileUploadService;
 import com.jeesite.modules.file.utils.FileUploadUtils;
+import com.jeesite.modules.mes.dao.MesIconsDao;
+import com.jeesite.modules.mes.entity.MesIcons;
+import com.jeesite.modules.mes.entity.MesIconsCategory;
+import com.jeesite.modules.sys.utils.UserUtils;
 
 /**
  * 功能模块Service
@@ -22,7 +35,15 @@ import com.jeesite.modules.file.utils.FileUploadUtils;
  */
 @Service
 public class MesIconsService extends CrudService<MesIconsDao, MesIcons> {
-	
+
+	/** 参数键:PDA 功能模块是否按分类分组(0=扁平 1=分组) */
+	public static final String CONFIG_ICONS_GROUPED = "mes.icons.grouped";
+
+	@Autowired
+	private MesIconsCategoryService mesIconsCategoryService;
+
+	@Resource
+	private FileUploadService fileUploadService;
 	/**
 	 * 获取单条数据
 	 * @param mesIcons
@@ -61,6 +82,9 @@ public class MesIconsService extends CrudService<MesIconsDao, MesIcons> {
 	@Override
 	@Transactional
 	public void save(MesIcons mesIcons) {
+		if (StringUtils.isBlank(mesIcons.getCategoryId())) {
+			mesIcons.setCategoryId(MesIconsCategory.DEFAULT_ID);
+		}
 		super.save(mesIcons);
 		// 保存上传图片
 		FileUploadUtils.saveFileUpload(mesIcons, mesIcons.getId(), "mesIcons_image");
@@ -88,5 +112,134 @@ public class MesIconsService extends CrudService<MesIconsDao, MesIcons> {
 		super.delete(mesIcons);
 		CacheUtils.put("mes_icons","icons","");
 	}
+
+	/**
+	 * PDA 功能模块列表(分组 / 扁平由参数设置 mes.icons.grouped 控制)
+	 * @param mesIcons 查询条件
+	 * @param host 图片 URL 前缀
+	 */
+	public List<Map<String, Object>> findIconsData(MesIcons mesIcons, String host) {
+		return findIconsData(mesIcons, host, isIconsGroupedEnabled());
+	}
+
+	/**
+	 * 是否启用按分类分组(参数设置:mes.icons.grouped,1=分组,0=扁平)
+	 */
+	public boolean isIconsGroupedEnabled() {
+		return "1".equals(Global.getConfig(CONFIG_ICONS_GROUPED, "1"));
+	}
+
+	/**
+	 * PDA 功能模块列表
+	 * @param grouped true=按分类分组,false=扁平列表(兼容旧版)
+	 */
+	private List<Map<String, Object>> findIconsData(MesIcons mesIcons, String host, boolean grouped) {
+		List<MesIcons> list = findList(mesIcons);
+		Subject subject = UserUtils.getSubject();
+		if (!grouped) {
+			return buildFlatIcons(list, host, subject);
+		}
+		return buildGroupedIcons(list, host, subject);
+	}
+
+	/**
+	 * 扁平列表(旧版格式)
+	 */
+	private List<Map<String, Object>> buildFlatIcons(List<MesIcons> list, String host, Subject subject) {
+		List<Map<String, Object>> mapList = ListUtils.newArrayList();
+		for (MesIcons mi : list) {
+			if (subject.isPermitted(mi.getAuth())) {
+				mapList.add(buildModuleMap(mi, host));
+			}
+		}
+		return mapList;
+	}
+
+	/**
+	 * 按分类分组列表(新版格式)
+	 */
+	private List<Map<String, Object>> buildGroupedIcons(List<MesIcons> list, String host, Subject subject) {
+		MesIconsCategory categoryQuery = new MesIconsCategory();
+		categoryQuery.setStatus(MesIconsCategory.STATUS_NORMAL);
+		List<MesIconsCategory> categories = mesIconsCategoryService.findList(categoryQuery);
+
+		Map<String, List<Map<String, Object>>> grouped = new LinkedHashMap<>();
+		for (MesIconsCategory category : categories) {
+			grouped.put(category.getId(), ListUtils.newArrayList());
+		}
+
+		for (MesIcons mi : list) {
+			if (!subject.isPermitted(mi.getAuth())) {
+				continue;
+			}
+			String categoryId = resolveCategoryId(mi.getCategoryId(), grouped);
+			grouped.computeIfAbsent(categoryId, k -> ListUtils.newArrayList()).add(buildModuleMap(mi, host));
+		}
+
+		List<Map<String, Object>> result = ListUtils.newArrayList();
+		for (MesIconsCategory category : categories) {
+			List<Map<String, Object>> children = grouped.get(category.getId());
+			if (ListUtils.isEmpty(children)) {
+				continue;
+			}
+			sortChildrenByTitle(children);
+			Map<String, Object> categoryMap = MapUtils.newHashMap();
+			categoryMap.put("categoryId", category.getId());
+			categoryMap.put("title", category.getTitle());
+			categoryMap.put("sort", category.getSortOrder() != null ? category.getSortOrder() : 0);
+			categoryMap.put("children", children);
+			result.add(categoryMap);
+		}
+		return result;
+	}
+
+	/**
+	 * children 按 title 字典序排列(中文按拼音)
+	 */
+	private void sortChildrenByTitle(List<Map<String, Object>> children) {
+		if (ListUtils.isEmpty(children) || children.size() <= 1) {
+			return;
+		}
+		Collator collator = Collator.getInstance(Locale.CHINA);
+		collator.setStrength(Collator.PRIMARY);
+		children.sort((a, b) -> collator.compare(
+				StringUtils.defaultString((String) a.get("title")),
+				StringUtils.defaultString((String) b.get("title"))));
+	}
+
+	/**
+	 * 解析模块所属分类,无效分类归入「其他」
+	 */
+	private String resolveCategoryId(String categoryId, Map<String, List<Map<String, Object>>> grouped) {
+		if (StringUtils.isNotBlank(categoryId) && grouped.containsKey(categoryId)) {
+			return categoryId;
+		}
+		return MesIconsCategory.DEFAULT_ID;
+	}
+
+	/**
+	 * 构建单个功能模块数据
+	 */
+	private Map<String, Object> buildModuleMap(MesIcons mi, String host) {
+		Map<String, Object> map = MapUtils.newHashMap();
+		map.put("title", mi.getTitle());
+		map.put("mode", mi.getMode());
+		map.put("img", buildModuleImageUrl(mi.getId(), host));
+		return map;
+	}
+
+	/**
+	 * 获取模块图标完整 URL
+	 */
+	private String buildModuleImageUrl(String bizKey, String host) {
+		FileUpload fu = new FileUpload();
+		fu.setBizKey(bizKey);
+		fu.setBizType("mesIcons_image");
+		List<FileUpload> finfo = fileUploadService.findList(fu);
+		if (ListUtils.isNotEmpty(finfo)) {
+			return host + finfo.get(0).getFileUrl();
+		}
+		return host;
+	}
 	
 }

+ 54 - 0
src/main/java/com/jeesite/modules/mes/service/MesProductGlueDurationService.java

@@ -0,0 +1,54 @@
+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.dao.MesProductGlueDurationDao;
+import com.jeesite.modules.mes.entity.MesProductGlueDuration;
+
+/**
+ * 涂胶加工时长Service
+ * @author mes
+ * @version 2026-07-13
+ */
+@Service
+public class MesProductGlueDurationService extends CrudService<MesProductGlueDurationDao, MesProductGlueDuration> {
+
+	@Override
+	public MesProductGlueDuration get(MesProductGlueDuration mesProductGlueDuration) {
+		return super.get(mesProductGlueDuration);
+	}
+
+	@Override
+	public Page<MesProductGlueDuration> findPage(MesProductGlueDuration mesProductGlueDuration) {
+		return super.findPage(mesProductGlueDuration);
+	}
+
+	@Override
+	public List<MesProductGlueDuration> findList(MesProductGlueDuration mesProductGlueDuration) {
+		return super.findList(mesProductGlueDuration);
+	}
+
+	@Override
+	@Transactional
+	public void save(MesProductGlueDuration mesProductGlueDuration) {
+		super.save(mesProductGlueDuration);
+	}
+
+	@Override
+	@Transactional
+	public void updateStatus(MesProductGlueDuration mesProductGlueDuration) {
+		super.updateStatus(mesProductGlueDuration);
+	}
+
+	@Override
+	@Transactional
+	public void delete(MesProductGlueDuration mesProductGlueDuration) {
+		super.delete(mesProductGlueDuration);
+	}
+
+}

+ 113 - 8
src/main/java/com/jeesite/modules/mes/web/ErpTransferDirectController.java

@@ -4,6 +4,10 @@ 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.erp.ErpMaterialUnitOption;
+import com.jeesite.modules.mes.erp.ErpProductionMaterialService;
+import com.jeesite.modules.mes.erp.ErpInventoryLotOption;
+import com.jeesite.modules.mes.erp.ErpInventoryLotService;
 import com.jeesite.modules.mes.erp.ErpPrdInStock;
 import com.jeesite.modules.mes.erp.ErpPrdInStockService;
 import com.jeesite.modules.mes.erp.ErpProductionOrder;
@@ -30,6 +34,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -49,6 +54,8 @@ public class ErpTransferDirectController extends BaseController {
 	private final ErpPrdInStockService erpPrdInStockService;
 	private final ErpStockService erpStockService;
 	private final ErpStockPdaService erpStockPdaService;
+	private final ErpInventoryLotService erpInventoryLotService;
+	private final ErpProductionMaterialService erpProductionMaterialService;
 	private final KingdeeErpProperties erpProperties;
 	private final MesLogService mesLogService;
 
@@ -58,6 +65,8 @@ public class ErpTransferDirectController extends BaseController {
 			ErpPrdInStockService erpPrdInStockService,
 			ErpStockService erpStockService,
 			ErpStockPdaService erpStockPdaService,
+			ErpInventoryLotService erpInventoryLotService,
+			ErpProductionMaterialService erpProductionMaterialService,
 			KingdeeErpProperties erpProperties,
 			MesLogService mesLogService) {
 		this.erpTransferDirectService = erpTransferDirectService;
@@ -66,6 +75,8 @@ public class ErpTransferDirectController extends BaseController {
 		this.erpPrdInStockService = erpPrdInStockService;
 		this.erpStockService = erpStockService;
 		this.erpStockPdaService = erpStockPdaService;
+		this.erpInventoryLotService = erpInventoryLotService;
+		this.erpProductionMaterialService = erpProductionMaterialService;
 		this.erpProperties = erpProperties;
 		this.mesLogService = mesLogService;
 	}
@@ -150,18 +161,12 @@ public class ErpTransferDirectController extends BaseController {
 
 	@RequiresPermissions("mes:erpTransferDirect:view")
 	@RequestMapping(value = "form")
-	public String form(String moBillNo, Model model) {
-		String billNo = StringUtils.trim(moBillNo);
-		List<ErpPrdInStock> sourceList = erpPrdInStockService.findTransferSourceByMoBillNo(billNo);
+	public String form(Model model) {
 		List<ErpStockOption> stockList = erpStockService.findUsableStockList();
-		model.addAttribute("moBillNo", billNo);
-		model.addAttribute("sourceList", sourceList);
 		model.addAttribute("stockList", stockList);
+		model.addAttribute("materialNumbers", erpProperties.getTransferDirectMaterialNumbers());
 		model.addAttribute("defaultSrcStockNumber", erpStockService.resolveDefaultStockNumber(stockList));
 		model.addAttribute("defaultDestStockNumber", StringUtils.trim(erpProperties.getTransferDirectDefaultDestStockNumber()));
-		if (sourceList.isEmpty()) {
-			model.addAttribute("message", "该生产订单暂无 HEV/115 生产入库行,请先完成生产入库");
-		}
 		return "modules/mes/erpTransferDirectForm";
 	}
 
@@ -273,6 +278,51 @@ public class ErpTransferDirectController extends BaseController {
 	}
 
 	@RequiresPermissions("mes:erpTransferDirect:view")
+	@RequestMapping(value = "materialInfo", method = {RequestMethod.GET, RequestMethod.POST})
+	@ResponseBody
+	public Map<String, Object> materialInfo(String materialNumber) {
+		Map<String, Object> result = new HashMap<>(4);
+		if (StringUtils.isBlank(materialNumber)) {
+			result.put("units", Collections.emptyList());
+			result.put("unitHint", "请先选择物料编码");
+			return result;
+		}
+		if (!erpProperties.isAllowedTransferDirectMaterial(materialNumber.trim())) {
+			result.put("units", Collections.emptyList());
+			result.put("unitHint", "物料不在 HEV/115 调拨范围内");
+			return result;
+		}
+		List<ErpMaterialUnitOption> units = erpProductionMaterialService.findMaterialUnitOptions(materialNumber);
+		result.put("units", units);
+		if (units.isEmpty()) {
+			result.put("unitHint", "未能从 ERP 加载单位,请手工填写单位编码");
+		} else if (units.size() == 1) {
+			result.put("defaultUnitNumber", units.get(0).getUnitNumber());
+			result.put("unitHint", "");
+		} else {
+			result.put("defaultUnitNumber", units.get(0).getUnitNumber());
+			result.put("unitHint", "该物料存在多个单位,请选择");
+		}
+		return result;
+	}
+
+	@RequiresPermissions("mes:erpTransferDirect:view")
+	@RequestMapping(value = "lotOptions", method = {RequestMethod.GET, RequestMethod.POST})
+	@ResponseBody
+	public Map<String, Object> lotOptions(String materialNumber, String stockNumber, String stockName,
+			BigDecimal pickQty) {
+		return buildLotOptions(materialNumber, stockNumber, stockName, pickQty);
+	}
+
+	@RequiresPermissions("mes:erpTransferDirect:view")
+	@RequestMapping(value = "lotSearch", method = {RequestMethod.GET, RequestMethod.POST})
+	@ResponseBody
+	public Map<String, Object> lotSearch(String materialNumber, String stockNumber, String stockName,
+			String lotKeyword) {
+		return buildLotSearch(materialNumber, stockNumber, stockName, lotKeyword);
+	}
+
+	@RequiresPermissions("mes:erpTransferDirect:view")
 	@RequestMapping(value = "stockLocOptions", method = {RequestMethod.GET, RequestMethod.POST})
 	@ResponseBody
 	public CommonResp<Map<String, Object>> stockLocOptions(String stockNumber, HttpServletRequest request) {
@@ -308,4 +358,59 @@ public class ErpTransferDirectController extends BaseController {
 			return ErpPdaRespHelper.fail(e.getMessage());
 		}
 	}
+
+	private Map<String, Object> buildLotOptions(String materialNumber, String stockNumber, String stockName,
+			BigDecimal pickQty) {
+		if (StringUtils.isBlank(materialNumber)) {
+			Map<String, Object> empty = new HashMap<>(2);
+			empty.put("lots", Collections.emptyList());
+			empty.put("lotHint", "请先选择物料编码");
+			return empty;
+		}
+		if (!erpProperties.isAllowedTransferDirectMaterial(materialNumber.trim())) {
+			Map<String, Object> empty = new HashMap<>(2);
+			empty.put("lots", Collections.emptyList());
+			empty.put("lotHint", "物料不在 HEV/115 调拨范围内");
+			return empty;
+		}
+		String stockStatus = erpProperties.getTransferDirectStockStatus();
+		String orgNumber = erpProperties.getOrgNumber();
+		List<ErpInventoryLotOption> lots = erpInventoryLotService.findAvailableLots(
+				materialNumber, stockNumber, stockStatus, orgNumber);
+		String suggested = erpInventoryLotService.suggestLotNumber(lots, pickQty, stockNumber);
+		Map<String, Object> result = new HashMap<>(4);
+		result.put("lots", lots);
+		result.put("suggestedLotNumber", suggested);
+		result.put("lotPickable", !lots.isEmpty());
+		if (lots.isEmpty()) {
+			result.put("lotHint", erpInventoryLotService.diagnoseLotShortage(
+					materialNumber, stockNumber, stockName, stockStatus, orgNumber));
+		} else {
+			result.put("lotHint", erpInventoryLotService.buildLotHint(lots, suggested, pickQty, stockNumber));
+		}
+		return result;
+	}
+
+	private Map<String, Object> buildLotSearch(String materialNumber, String stockNumber, String stockName,
+			String lotKeyword) {
+		if (StringUtils.isBlank(materialNumber)) {
+			Map<String, Object> empty = new HashMap<>(2);
+			empty.put("lots", Collections.emptyList());
+			empty.put("lotHint", "请先选择物料编码");
+			return empty;
+		}
+		String stockStatus = erpProperties.getTransferDirectStockStatus();
+		String orgNumber = erpProperties.getOrgNumber();
+		List<ErpInventoryLotOption> lots = erpInventoryLotService.searchInventoryLots(
+				materialNumber, stockNumber, stockStatus, orgNumber, lotKeyword);
+		Map<String, Object> result = new HashMap<>(3);
+		result.put("lots", lots);
+		if (lots.isEmpty()) {
+			String diagnostic = erpInventoryLotService.getLastQueryDiagnostic();
+			result.put("lotHint", StringUtils.isNotBlank(diagnostic) ? diagnostic : "未查到符合条件的批号库存");
+		} else {
+			result.put("lotHint", "共查到 " + lots.size() + " 条");
+		}
+		return result;
+	}
 }

+ 50 - 286
src/main/java/com/jeesite/modules/mes/web/MesDeviceController.java

@@ -21,7 +21,6 @@ import com.jeesite.modules.mes.entity.MesLineProcess;
 import com.jeesite.modules.mes.resp.CommonResp;
 import com.jeesite.modules.mes.service.MesDeviceCateService;
 import com.jeesite.modules.mes.service.MesDevicePreventTaskService;
-import com.jeesite.modules.mes.service.MesDeviceStateService;
 import com.jeesite.modules.mes.service.MesDeviceUrgencyService;
 import org.apache.shiro.authz.annotation.RequiresPermissions;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -30,6 +29,7 @@ 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.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
 
@@ -45,6 +45,9 @@ import com.jeesite.common.utils.excel.annotation.ExcelField.Type;
 import org.springframework.web.multipart.MultipartFile;
 import com.jeesite.common.web.BaseController;
 import com.jeesite.modules.mes.entity.MesDevice;
+import com.jeesite.modules.mes.req.MesDeviceReportStateReq;
+import com.jeesite.modules.mes.service.MesDeviceOeeStatsService;
+import com.jeesite.modules.mes.service.MesDeviceStateReportService;
 import com.jeesite.modules.mes.service.MesDeviceService;
 
 /**
@@ -66,10 +69,13 @@ public class MesDeviceController extends BaseController {
 	private MesDevicePreventTaskService mesDevicePreventTaskService;
 
 	@Autowired
-	private MesDeviceStateService mesDeviceStateService;
+	private MesDeviceUrgencyService mesDeviceUrgencyService;
 
 	@Autowired
-	private MesDeviceUrgencyService mesDeviceUrgencyService;
+	private MesDeviceStateReportService mesDeviceStateReportService;
+
+	@Autowired
+	private MesDeviceOeeStatsService mesDeviceOeeStatsService;
 
 	/**
 	 * 获取数据
@@ -414,6 +420,46 @@ public class MesDeviceController extends BaseController {
 		return "modules/mes/mesDeviceOeeScreen";
 	}
 
+	/**
+	 * 工位客户端按 gw + lineSn 上报设备状态(无需登录)
+	 */
+	@PostMapping(value = "reportState")
+	@ResponseBody
+	public CommonResp<Map<String, Object>> reportState(@RequestBody(required = false) MesDeviceReportStateReq req, HttpServletRequest request) {
+		CommonResp<Map<String, Object>> resp = new CommonResp<>();
+		if (req == null) {
+			req = new MesDeviceReportStateReq();
+		}
+		if (StringUtils.isBlank(req.getGw())) {
+			req.setGw(request.getParameter("gw"));
+		}
+		if (StringUtils.isBlank(req.getLineSn())) {
+			req.setLineSn(request.getParameter("lineSn"));
+		}
+		if (StringUtils.isBlank(req.getState())) {
+			req.setState(request.getParameter("state"));
+		}
+		if (StringUtils.isBlank(req.getContent())) {
+			req.setContent(request.getParameter("content"));
+		}
+		try {
+			MesDevice device = mesDeviceStateReportService.reportByGw(req.getGw(), req.getLineSn(), req.getState(), req.getContent());
+			Map<String, Object> data = MapUtils.newHashMap();
+			data.put("deviceSn", device.getSn());
+			data.put("gw", device.getGw());
+			data.put("lineSn", device.getLineSn());
+			data.put("state", req.getState());
+			resp.setData(data);
+			resp.setResult(Global.TRUE);
+			resp.setMessage(text("操作成功!"));
+		} catch (Exception e) {
+			logger.error("设备状态上报失败", e);
+			resp.setResult(Global.FALSE);
+			resp.setMessage(e.getMessage());
+		}
+		return resp;
+	}
+
 	@RequestMapping(value = "screen5Data")
 	@ResponseBody
 	public CommonResp<Map<String, Object>> screen5Data() {
@@ -510,291 +556,9 @@ public class MesDeviceController extends BaseController {
 	@ResponseBody
 	public CommonResp<Map<String, Object>> oeeScreenData() {
 		CommonResp<Map<String, Object>> resp = new CommonResp<>();
-		Map<String, Object> data = MapUtils.newHashMap();
-		Date now = new Date();
-		Calendar calendar = Calendar.getInstance();
-		calendar.set(Calendar.HOUR_OF_DAY, 0);
-		calendar.set(Calendar.MINUTE, 0);
-		calendar.set(Calendar.SECOND, 0);
-		calendar.set(Calendar.MILLISECOND, 0);
-		Date todayStart = calendar.getTime();
-		long elapsedSeconds = Math.max(1L, (now.getTime() - todayStart.getTime()) / 1000);
-
-		List<MesDevice> devices = mesDeviceService.findList(new MesDevice());
-		List<Map<String, Object>> deviceCards = ListUtils.newArrayList();
-		List<Map<String, Object>> ranking = ListUtils.newArrayList();
-		long runningCount = 0;
-		long faultCount = 0;
-		long maintenanceCount = 0;
-		long alarmCount = 0;
-		long offlineCount = 0;
-		long unknownCount = 0;
-		long totalRunningSeconds = 0;
-
-		for (int i = 0; i < devices.size(); i++) {
-			MesDevice device = devices.get(i);
-			MesDeviceState latestQuery = new MesDeviceState();
-			latestQuery.setDeviceSn(device.getSn());
-			MesDeviceState latestState = StringUtils.isBlank(device.getSn()) ? null : mesDeviceStateService.findInfo(latestQuery);
-			String state = latestState != null && StringUtils.isNotBlank(latestState.getState()) ? latestState.getState() : device.getWorkStatus();
-			String statusCode = getDeviceScreenStatus(device, state);
-			if ("running".equals(statusCode)) {
-				runningCount++;
-			} else if ("fault".equals(statusCode)) {
-				faultCount++;
-			} else if ("maintenance".equals(statusCode)) {
-				maintenanceCount++;
-			} else if ("alarm".equals(statusCode)) {
-				alarmCount++;
-			} else if ("offline".equals(statusCode)) {
-				offlineCount++;
-			} else {
-				unknownCount++;
-			}
-
-			MesDeviceState stateQuery = new MesDeviceState();
-			stateQuery.setDeviceSn(device.getSn());
-			stateQuery.setStartDate_gte(todayStart);
-			stateQuery.setStartDate_lte(now);
-			List<MesDeviceState> stateList = StringUtils.isBlank(device.getSn()) ? ListUtils.newArrayList() : mesDeviceStateService.findList(stateQuery);
-			long runningSeconds = getRunningSeconds(stateList, todayStart, now);
-			if (isRunningState(state) && latestState != null && latestState.getStartDate() != null && latestState.getStartDate().before(todayStart)) {
-				runningSeconds = elapsedSeconds;
-			}
-			if (runningSeconds <= 0 && isRunningState(state)) {
-				runningSeconds = getRunningSecondsFromWorkTime(device.getWorkTime(), todayStart, now);
-			}
-			totalRunningSeconds += runningSeconds;
-			double openRate = Math.min(100D, runningSeconds * 100D / elapsedSeconds);
-
-			Map<String, Object> card = MapUtils.newHashMap();
-			card.put("name", getText(device.getTreeName(), getText(device.getSn(), "-")));
-			card.put("sn", getText(device.getSn(), "-"));
-			card.put("deviceCode", getText(device.getDeviceCode(), "-"));
-			card.put("lineSn", getText(device.getLineSn(), "-"));
-			card.put("imagePath", getFirstImagePath(device.getImagePath()));
-			card.put("statusCode", statusCode);
-			card.put("statusName", getDeviceScreenStatusName(statusCode));
-			card.put("stateName", getDeviceStateName(state));
-			card.put("openRate", Math.round(openRate * 10D) / 10D);
-			card.put("openMinutes", Math.round(runningSeconds / 60D));
-			String lastReportDate = "-";
-			if (latestState != null && latestState.getStartDate() != null) {
-				lastReportDate = DateUtils.formatDate(latestState.getStartDate(), "yyyy-MM-dd HH:mm:ss");
-			} else {
-				Date workTime = parseWorkTime(device.getWorkTime());
-				if (workTime != null) {
-					lastReportDate = DateUtils.formatDate(workTime, "yyyy-MM-dd HH:mm:ss");
-				}
-			}
-			card.put("lastReportTime", lastReportDate);
-			card.put("lastReportDate", lastReportDate);
-			card.put("reportContent", getDeviceReportContent(latestState, state, statusCode));
-			card.put("imageIndex", i % 8 + 1);
-			deviceCards.add(card);
-
-			Map<String, Object> rankItem = MapUtils.newHashMap();
-			rankItem.put("name", card.get("name"));
-			rankItem.put("sn", card.get("sn"));
-			rankItem.put("openRate", card.get("openRate"));
-			rankItem.put("openMinutes", card.get("openMinutes"));
-			ranking.add(rankItem);
-		}
-
-		ranking.sort((a, b) -> Double.compare(Double.valueOf(String.valueOf(b.get("openRate"))), Double.valueOf(String.valueOf(a.get("openRate")))));
-		if (ranking.size() > 10) {
-			ranking = ranking.subList(0, 10);
-		}
-
-		List<Map<String, Object>> statusList = ListUtils.newArrayList();
-		addStatusItem(statusList, "运行", runningCount, "#35f2c2");
-		addStatusItem(statusList, "故障", faultCount, "#ff5f6d");
-		addStatusItem(statusList, "维修", maintenanceCount, "#ffc857");
-		addStatusItem(statusList, "报警", alarmCount, "#ff8a3d");
-		addStatusItem(statusList, "离线", offlineCount, "#7d8da8");
-		addStatusItem(statusList, "未知", unknownCount, "#49a7ff");
-
-		double avgOpenRate = devices.isEmpty() ? 0D : totalRunningSeconds * 100D / (elapsedSeconds * devices.size());
-		data.put("deviceTotal", devices.size());
-		data.put("runningCount", runningCount);
-		data.put("faultCount", faultCount);
-		data.put("offlineCount", offlineCount);
-		data.put("avgOpenRate", Math.round(avgOpenRate * 10D) / 10D);
-		data.put("statusList", statusList);
-		data.put("ranking", ranking);
-		data.put("devices", deviceCards);
-		data.put("statDate", DateUtils.formatDate(now, "yyyy-MM-dd"));
-		data.put("statTime", DateUtils.formatDate(now, "HH:mm:ss"));
-		data.put("dataNote", "当前系统缺少计划产量、良品数、理想节拍,页面展示的是基于设备状态记录计算的开动率。");
-		resp.setData(data);
+		resp.setData(mesDeviceOeeStatsService.buildOeeScreenData());
 		resp.setResult(Global.TRUE);
 		return resp;
 	}
 
-	private long getRunningSeconds(List<MesDeviceState> stateList, Date todayStart, Date now) {
-		long runningSeconds = 0L;
-		for (MesDeviceState state : stateList) {
-			if (!isRunningState(state.getState()) || state.getStartDate() == null) {
-				continue;
-			}
-			Date start = state.getStartDate().before(todayStart) ? todayStart : state.getStartDate();
-			Date end = state.getEndDate();
-			if (end == null && state.getInterval() != null) {
-				end = new Date(state.getStartDate().getTime() + state.getInterval() * 1000L);
-			}
-			if (end == null || end.after(now)) {
-				end = now;
-			}
-			if (end.after(start)) {
-				runningSeconds += (end.getTime() - start.getTime()) / 1000;
-			}
-		}
-		return runningSeconds;
-	}
-
-	private long getRunningSecondsFromWorkTime(String workTime, Date todayStart, Date now) {
-		if (StringUtils.isBlank(workTime)) {
-			return 0L;
-		}
-		Date start = parseWorkTime(workTime);
-		if (start == null) {
-			return 0L;
-		}
-		if (start.before(todayStart)) {
-			start = todayStart;
-		}
-		if (!now.after(start)) {
-			return 0L;
-		}
-		return (now.getTime() - start.getTime()) / 1000;
-	}
-
-	private Date parseWorkTime(String workTime) {
-		String[] patterns = {"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm"};
-		for (String pattern : patterns) {
-			try {
-				return new SimpleDateFormat(pattern).parse(workTime);
-			} catch (Exception ignored) {
-			}
-		}
-		return null;
-	}
-
-	private boolean isRunningState(String state) {
-		return "1".equals(state) || "2".equals(state);
-	}
-
-	private String getDeviceScreenStatus(MesDevice device, String state) {
-		if (device.getOnline() != null && device.getOnline() == 1) {
-			return "offline";
-		}
-		if ("3".equals(state)) {
-			return "fault";
-		}
-		if ("4".equals(state)) {
-			return "maintenance";
-		}
-		if ("5".equals(state) || "6".equals(state)) {
-			return "alarm";
-		}
-		if (isRunningState(state)) {
-			return "running";
-		}
-		return "unknown";
-	}
-
-	private String getDeviceScreenStatusName(String statusCode) {
-		if ("running".equals(statusCode)) {
-			return "运行";
-		}
-		if ("fault".equals(statusCode)) {
-			return "故障";
-		}
-		if ("maintenance".equals(statusCode)) {
-			return "维修";
-		}
-		if ("alarm".equals(statusCode)) {
-			return "报警";
-		}
-		if ("offline".equals(statusCode)) {
-			return "离线";
-		}
-		return "未知";
-	}
-
-	private String getDeviceReportContent(MesDeviceState latestState, String state, String statusCode) {
-		if (latestState != null && StringUtils.isNotBlank(latestState.getContent())) {
-			return latestState.getContent().trim();
-		}
-		if ("offline".equals(statusCode)) {
-			return "设备离线";
-		}
-		if ("1".equals(state)) {
-			return "开机运行正常";
-		}
-		if ("2".equals(state)) {
-			return "手动运行中";
-		}
-		if ("3".equals(state)) {
-			return "设备故障";
-		}
-		if ("4".equals(state)) {
-			return "设备维修中";
-		}
-		if ("5".equals(state)) {
-			return "质量报警";
-		}
-		if ("6".equals(state)) {
-			return "物流堵料";
-		}
-		return "暂无上报信息";
-	}
-
-	private String getDeviceStateName(String state) {
-		if ("1".equals(state)) {
-			return "正常";
-		}
-		if ("2".equals(state)) {
-			return "手动";
-		}
-		if ("3".equals(state)) {
-			return "故障";
-		}
-		if ("4".equals(state)) {
-			return "维修";
-		}
-		if ("5".equals(state)) {
-			return "质量报警";
-		}
-		if ("6".equals(state)) {
-			return "物流堵料";
-		}
-		return "未知";
-	}
-
-	private void addStatusItem(List<Map<String, Object>> list, String name, long value, String color) {
-		Map<String, Object> item = MapUtils.newHashMap();
-		item.put("name", name);
-		item.put("value", value);
-		item.put("color", color);
-		list.add(item);
-	}
-
-	private String getText(String value, String defaultValue) {
-		return StringUtils.isBlank(value) ? defaultValue : value;
-	}
-
-	private String getFirstImagePath(String imagePath) {
-		if (StringUtils.isBlank(imagePath)) {
-			return "";
-		}
-		String[] paths = imagePath.split("\\|");
-		for (String path : paths) {
-			if (StringUtils.isNotBlank(path)) {
-				return path;
-			}
-		}
-		return "";
-	}
-
 }

+ 95 - 0
src/main/java/com/jeesite/modules/mes/web/MesDocumentController.java

@@ -14,10 +14,17 @@ 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.codec.EncodeUtils;
+import com.jeesite.common.collect.ListUtils;
 import com.jeesite.common.config.Global;
 import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.service.ServiceException;
 import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.file.entity.FileUpload;
+import com.jeesite.modules.file.service.FileUploadService;
 import com.jeesite.modules.mes.entity.MesDocument;
+import com.jeesite.modules.mes.service.MesDocumentPreviewService;
 import com.jeesite.modules.mes.service.MesDocumentService;
 
 /**
@@ -31,6 +38,12 @@ public class MesDocumentController extends BaseController {
 
 	@Autowired
 	private MesDocumentService mesDocumentService;
+
+	@Autowired
+	private FileUploadService fileUploadService;
+
+	@Autowired
+	private MesDocumentPreviewService mesDocumentPreviewService;
 	
 	/**
 	 * 获取数据
@@ -59,10 +72,92 @@ public class MesDocumentController extends BaseController {
 	public Page<MesDocument> listData(MesDocument mesDocument, HttpServletRequest request, HttpServletResponse response) {
 		mesDocument.setPage(new Page<>(request, response));
 		Page<MesDocument> page = mesDocumentService.findPage(mesDocument);
+		fillPreviewInfo(page.getList(), request);
 		return page;
 	}
 
 	/**
+	 * 填充列表中的附件预览信息
+	 */
+	private void fillPreviewInfo(List<MesDocument> list, HttpServletRequest request) {
+		if (ListUtils.isEmpty(list)) {
+			return;
+		}
+		for (MesDocument doc : list) {
+			doc.setPreviewable(false);
+			FileUpload fu = new FileUpload();
+			fu.setBizKey(doc.getId());
+			fu.setBizType("mesDocument_file");
+			List<FileUpload> files = fileUploadService.findList(fu);
+			if (ListUtils.isEmpty(files)) {
+				continue;
+			}
+			for (FileUpload file : files) {
+				if (!mesDocumentPreviewService.isPreviewable(file.getFileName())) {
+					continue;
+				}
+				doc.setFileUploadId(file.getId());
+				doc.setFileName(file.getFileName());
+				doc.setPreviewable(true);
+				doc.setPreviewUrl(buildPreviewUrl(file.getId(), request));
+				break;
+			}
+		}
+	}
+
+	/**
+	 * 构建预览页面地址
+	 */
+	private String buildPreviewUrl(String fileUploadId, HttpServletRequest request) {
+		return request.getContextPath() + Global.getAdminPath()
+				+ "/mes/mesDocument/preview?fileUploadId=" + fileUploadId;
+	}
+
+	/**
+	 * 文档预览页面(PDF / docx / xlsx)
+	 */
+	@RequiresPermissions("mes:mesDocument:view")
+	@RequestMapping(value = "preview")
+	public String preview(String fileUploadId, Model model, HttpServletRequest request) {
+		if (StringUtils.isBlank(fileUploadId)) {
+			throw new ServiceException("预览参数无效");
+		}
+		FileUpload fileUpload = fileUploadService.get(fileUploadId);
+		if (fileUpload == null) {
+			throw new ServiceException("附件不存在或已被删除");
+		}
+		String fileName = StringUtils.defaultString(fileUpload.getFileName(), "");
+		String previewType = mesDocumentPreviewService.getPreviewType(fileName);
+		String ctxAdmin = request.getContextPath() + Global.getAdminPath();
+		model.addAttribute("fileName", fileName);
+		model.addAttribute("previewType", previewType);
+		model.addAttribute("fileUrl", ctxAdmin + "/mes/mesDocument/previewFile?fileUploadId=" + fileUploadId);
+		model.addAttribute("downloadUrl", ctxAdmin + "/file/download/" + fileUploadId
+				+ "?fileName=" + EncodeUtils.encodeUrl(fileName));
+		return "modules/mes/mesDocumentPreview";
+	}
+
+	/**
+	 * 输出附件原始文件流
+	 */
+	@RequiresPermissions("mes:mesDocument:view")
+	@RequestMapping(value = "previewFile")
+	public void previewFile(String fileUploadId, HttpServletResponse response) {
+		if (StringUtils.isBlank(fileUploadId)) {
+			throw new ServiceException("预览参数无效");
+		}
+		FileUpload fileUpload = fileUploadService.get(fileUploadId);
+		try {
+			mesDocumentPreviewService.writeFileStream(fileUpload, response);
+		} catch (ServiceException e) {
+			throw e;
+		} catch (Exception e) {
+			logger.error("文档预览失败, fileUploadId={}", fileUploadId, e);
+			throw new ServiceException("文档预览失败:" + e.getMessage());
+		}
+	}
+
+	/**
 	 * 查看编辑表单
 	 */
 	@RequiresPermissions("mes:mesDocument:view")

+ 117 - 0
src/main/java/com/jeesite/modules/mes/web/MesIconsCategoryController.java

@@ -0,0 +1,117 @@
+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.web.BaseController;
+import com.jeesite.modules.mes.entity.MesIconsCategory;
+import com.jeesite.modules.mes.service.MesIconsCategoryService;
+
+/**
+ * 功能模块分类Controller
+ * @author hzd
+ * @version 2026-07-09
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/mesIconsCategory")
+public class MesIconsCategoryController extends BaseController {
+
+	@Autowired
+	private MesIconsCategoryService mesIconsCategoryService;
+
+	@ModelAttribute
+	public MesIconsCategory get(String id, boolean isNewRecord) {
+		return mesIconsCategoryService.get(id, isNewRecord);
+	}
+
+	/**
+	 * 查询列表
+	 */
+	@RequiresPermissions("mes:mesIconsCategory:view")
+	@RequestMapping(value = {"list", ""})
+	public String list(MesIconsCategory mesIconsCategory, Model model) {
+		model.addAttribute("mesIconsCategory", mesIconsCategory);
+		model.addAttribute("defaultCategoryId", MesIconsCategory.DEFAULT_ID);
+		return "modules/mes/mesIconsCategoryList";
+	}
+
+	/**
+	 * 查询列表数据
+	 */
+	@RequiresPermissions("mes:mesIconsCategory:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MesIconsCategory> listData(MesIconsCategory mesIconsCategory, HttpServletRequest request, HttpServletResponse response) {
+		mesIconsCategory.setPage(new Page<>(request, response));
+		return mesIconsCategoryService.findPage(mesIconsCategory);
+	}
+
+	/**
+	 * 查看编辑表单
+	 */
+	@RequiresPermissions("mes:mesIconsCategory:view")
+	@RequestMapping(value = "form")
+	public String form(MesIconsCategory mesIconsCategory, Model model) {
+		model.addAttribute("mesIconsCategory", mesIconsCategory);
+		return "modules/mes/mesIconsCategoryForm";
+	}
+
+	/**
+	 * 保存数据
+	 */
+	@RequiresPermissions("mes:mesIconsCategory:edit")
+	@PostMapping(value = "save")
+	@ResponseBody
+	public String save(@Validated MesIconsCategory mesIconsCategory) {
+		mesIconsCategoryService.save(mesIconsCategory);
+		return renderResult(Global.TRUE, text("保存功能模块分类成功!"));
+	}
+
+	/**
+	 * 停用数据
+	 */
+	@RequiresPermissions("mes:mesIconsCategory:edit")
+	@RequestMapping(value = "disable")
+	@ResponseBody
+	public String disable(MesIconsCategory mesIconsCategory) {
+		mesIconsCategory.setStatus(MesIconsCategory.STATUS_DISABLE);
+		mesIconsCategoryService.updateStatus(mesIconsCategory);
+		return renderResult(Global.TRUE, text("停用功能模块分类成功"));
+	}
+
+	/**
+	 * 启用数据
+	 */
+	@RequiresPermissions("mes:mesIconsCategory:edit")
+	@RequestMapping(value = "enable")
+	@ResponseBody
+	public String enable(MesIconsCategory mesIconsCategory) {
+		mesIconsCategory.setStatus(MesIconsCategory.STATUS_NORMAL);
+		mesIconsCategoryService.updateStatus(mesIconsCategory);
+		return renderResult(Global.TRUE, text("启用功能模块分类成功"));
+	}
+
+	/**
+	 * 删除数据
+	 */
+	@RequiresPermissions("mes:mesIconsCategory:edit")
+	@RequestMapping(value = "delete")
+	@ResponseBody
+	public String delete(MesIconsCategory mesIconsCategory) {
+		mesIconsCategoryService.delete(mesIconsCategory);
+		return renderResult(Global.TRUE, text("删除功能模块分类成功!"));
+	}
+
+}

+ 22 - 24
src/main/java/com/jeesite/modules/mes/web/MesIconsController.java

@@ -33,8 +33,11 @@ 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.MesIcons;
+import com.jeesite.modules.mes.entity.MesIconsCategory;
+import com.jeesite.modules.mes.service.MesIconsCategoryService;
 import com.jeesite.modules.mes.service.MesIconsService;
 
 /**
@@ -49,6 +52,9 @@ public class MesIconsController extends BaseController {
 	@Autowired
 	private MesIconsService mesIconsService;
 
+	@Autowired
+	private MesIconsCategoryService mesIconsCategoryService;
+
 	@Resource
 	protected FileUploadService fileUploadService;
     @Autowired
@@ -63,12 +69,22 @@ public class MesIconsController extends BaseController {
 	}
 
 	/**
+	 * 加载功能模块分类下拉列表
+	 */
+	private void loadCategoryList(Model model) {
+		MesIconsCategory query = new MesIconsCategory();
+		query.setStatus(MesIconsCategory.STATUS_NORMAL);
+		model.addAttribute("categoryList", mesIconsCategoryService.findList(query));
+	}
+
+	/**
 	 * 查询列表
 	 */
 	@RequiresPermissions("mes:mesIcons:view")
 	@RequestMapping(value = {"list", ""})
 	public String list(MesIcons mesIcons, Model model) {
 		model.addAttribute("mesIcons", mesIcons);
+		loadCategoryList(model);
 		return "modules/mes/mesIconsList";
 	}
 
@@ -90,31 +106,9 @@ public class MesIconsController extends BaseController {
 
 	@RequestMapping(value = "icons")
 	@ResponseBody
-	public CommonResp icons(MesIcons mesIcons,HttpServletRequest req) {
+	public CommonResp icons(MesIcons mesIcons, HttpServletRequest req) {
 		String host = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/js";
-		List<MesIcons> list = mesIconsService.findList(mesIcons);
-		List<Map<String, Object>> mapList = ListUtils.newArrayList();
-		Subject subject = UserUtils.getSubject();
-		for (MesIcons mi : list) {
-			if(subject.isPermitted(mi.getAuth())){
-				Map<String, Object> map = MapUtils.newHashMap();
-				map.put("title", mi.getTitle());
-				map.put("mode", mi.getMode());
-				// 获取图片
-				FileUpload fu = new FileUpload();
-				fu.setBizKey(mi.getId());
-				fu.setBizType("mesIcons_image");
-				List<FileUpload> finfo = fileUploadService.findList(fu);
-				if(finfo!=null&&finfo.size()>0){
-					map.put("img", host + finfo.get(0).getFileUrl());
-				}else{
-					map.put("img", host + "");
-				}
-
-				mapList.add(map);
-			}
-		}
-
+		List<Map<String, Object>> mapList = mesIconsService.findIconsData(mesIcons, host);
 		CommonResp<List<Map<String, Object>>> resp = new CommonResp<>();
 		resp.setData(mapList);
 		resp.setResult(Global.TRUE);
@@ -170,7 +164,11 @@ public class MesIconsController extends BaseController {
 	@RequiresPermissions("mes:mesIcons:view")
 	@RequestMapping(value = "form")
 	public String form(MesIcons mesIcons, Model model) {
+		if (mesIcons.getIsNewRecord() && StringUtils.isBlank(mesIcons.getCategoryId())) {
+			mesIcons.setCategoryId(MesIconsCategory.DEFAULT_ID);
+		}
 		model.addAttribute("mesIcons", mesIcons);
+		loadCategoryList(model);
 		return "modules/mes/mesIconsForm";
 	}
 

+ 28 - 3
src/main/java/com/jeesite/modules/mes/web/MesProductArcWeldingController.java

@@ -71,11 +71,32 @@ public class MesProductArcWeldingController extends BaseController {
 	}
 	
 	/**
-	 * 查询列表
+	 * 查询列表(兼容旧菜单,跳转点焊参数)
 	 */
 	@RequiresPermissions("mes:mesProductArcWelding:view")
 	@RequestMapping(value = {"list", ""})
 	public String list(MesProductArcWelding mesProductArcWelding, Model model) {
+		return listSpot(mesProductArcWelding, model);
+	}
+
+	/**
+	 * 点焊参数列表
+	 */
+	@RequiresPermissions("mes:mesProductArcWelding:view")
+	@RequestMapping(value = "listSpot")
+	public String listSpot(MesProductArcWelding mesProductArcWelding, Model model) {
+		mesProductArcWelding.setWeldingCategory("spot");
+		model.addAttribute("mesProductArcWelding", mesProductArcWelding);
+		return "modules/mes/mesProductSpotWeldingList";
+	}
+
+	/**
+	 * 弧焊参数列表
+	 */
+	@RequiresPermissions("mes:mesProductArcWelding:view")
+	@RequestMapping(value = "listArc")
+	public String listArc(MesProductArcWelding mesProductArcWelding, Model model) {
+		mesProductArcWelding.setWeldingCategory("arc");
 		model.addAttribute("mesProductArcWelding", mesProductArcWelding);
 		return "modules/mes/mesProductArcWeldingList";
 	}
@@ -99,6 +120,9 @@ public class MesProductArcWeldingController extends BaseController {
 		}
 		Map<String, String> oprnoTitleMap = new HashMap<>();
 		for (MesProductArcWelding record : list) {
+			if (StringUtils.isNotBlank(record.getOprnoTitle())) {
+				continue;
+			}
 			if (StringUtils.isBlank(record.getLineSn()) || StringUtils.isBlank(record.getOprno())) {
 				record.setOprnoTitle("");
 				continue;
@@ -157,8 +181,9 @@ public class MesProductArcWeldingController extends BaseController {
 	public void exportData(MesProductArcWelding mesProductArcWelding, HttpServletResponse response) {
 		List<MesProductArcWelding> list = mesProductArcWeldingService.findList(mesProductArcWelding);
 		fillOprnoTitle(list);
-		String fileName = "弧焊参数" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
-		try(ExcelExport ee = new ExcelExport("弧焊参数", MesProductArcWelding.class)){
+		String exportTitle = "arc".equals(mesProductArcWelding.getWeldingCategory()) ? "弧焊参数" : "点焊参数";
+		String fileName = exportTitle + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+		try(ExcelExport ee = new ExcelExport(exportTitle, MesProductArcWelding.class)){
 			ee.setDataList(list).write(response, fileName);
 		}
 	}

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

@@ -104,6 +104,9 @@ public class MesProductController extends BaseController {
 	@Autowired
 	private MesBatchSnService mesBatchSnService;
 
+	@Autowired
+	private MesDeviceOeeStatsService mesDeviceOeeStatsService;
+
 
 	/**
 	 * 获取数据
@@ -937,31 +940,30 @@ public class MesProductController extends BaseController {
 		MesDevice mesDevice = new MesDevice();
 		Long deviceNum = mesDeviceService.findCount(mesDevice);
 		data.put("devices", deviceNum);
-		List<MesDevice> deviceList = mesDeviceService.findList(new MesDevice());
-		int onlineDeviceCount = 0;
-		int offlineDeviceCount = 0;
+		Map<String, Object> deviceStats = mesDeviceOeeStatsService.buildOeeScreenData();
+		data.put("deviceTotal", deviceStats.get("deviceTotal"));
+		data.put("runningDevices", deviceStats.get("runningDevices"));
+		data.put("stoppedDevices", deviceStats.get("stoppedDevices"));
+		data.put("onlineDevices", deviceStats.get("runningDevices"));
+		data.put("idleDevices", deviceStats.get("stoppedDevices"));
+		data.put("offlineDevices", deviceStats.get("otherDeviceCount"));
+		data.put("avgOpenRate", deviceStats.get("avgOpenRate"));
+		data.put("runningRate", deviceStats.get("runningRate"));
+		data.put("deviceOeeCards", deviceStats.get("devices"));
 		List<Map<String, Object>> deviceStatusRows = new ArrayList<>();
-		if (deviceList != null) {
-			for (MesDevice device : deviceList) {
-				boolean online = device.getOnline() != null && device.getOnline() == 0;
-				if (online) {
-					onlineDeviceCount++;
-				} else {
-					offlineDeviceCount++;
-				}
-				if (deviceStatusRows.size() < 6) {
-					Map<String, Object> item = MapUtils.newHashMap();
-					item.put("name", safeText(device.getTreeName(), safeText(device.getSn(), "-")));
-					item.put("sn", safeText(device.getSn(), "-"));
-					item.put("lineSn", safeText(device.getLineSn(), "-"));
-					item.put("online", online);
-					deviceStatusRows.add(item);
-				}
+		List<Map<String, Object>> deviceCards = (List<Map<String, Object>>) deviceStats.get("devices");
+		if (deviceCards != null) {
+			for (int i = 0; i < deviceCards.size() && deviceStatusRows.size() < 6; i++) {
+				Map<String, Object> card = deviceCards.get(i);
+				Map<String, Object> item = MapUtils.newHashMap();
+				item.put("name", card.get("name"));
+				item.put("sn", card.get("sn"));
+				item.put("lineSn", card.get("lineSn"));
+				item.put("online", "running".equals(String.valueOf(card.get("statusCode"))));
+				item.put("statusName", card.get("statusName"));
+				deviceStatusRows.add(item);
 			}
 		}
-		data.put("deviceTotal", deviceNum);
-		data.put("onlineDevices", onlineDeviceCount);
-		data.put("offlineDevices", offlineDeviceCount);
 		data.put("deviceStatusRows", deviceStatusRows);
 
 		MesProduct mesProduct1 = new MesProduct();

+ 132 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductGlueDurationController.java

@@ -0,0 +1,132 @@
+package com.jeesite.modules.mes.web;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.jeesite.common.collect.ListUtils;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.modules.mes.entity.MesLineProcess;
+import com.jeesite.modules.mes.service.MesLineProcessService;
+import com.jeesite.modules.mes.util.CommonUitl;
+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.util.ObjectUtils;
+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.web.BaseController;
+import com.jeesite.modules.mes.entity.MesProductGlueDuration;
+import com.jeesite.modules.mes.service.MesProductGlueDurationService;
+
+/**
+ * 涂胶加工时长Controller
+ * @author mes
+ * @version 2026-07-13
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/mesProductGlueDuration")
+public class MesProductGlueDurationController extends BaseController {
+
+	@Autowired
+	private MesProductGlueDurationService mesProductGlueDurationService;
+
+	@Autowired
+	private MesLineProcessService mesLineProcessService;
+
+	@ModelAttribute
+	public MesProductGlueDuration get(String id, boolean isNewRecord) {
+		return mesProductGlueDurationService.get(id, isNewRecord);
+	}
+
+	@RequiresPermissions("mes:mesProductGlueDuration:view")
+	@RequestMapping(value = {"list", ""})
+	public String list(MesProductGlueDuration mesProductGlueDuration, Model model) {
+		model.addAttribute("mesProductGlueDuration", mesProductGlueDuration);
+		return "modules/mes/mesProductGlueDurationList";
+	}
+
+	@RequiresPermissions("mes:mesProductGlueDuration:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MesProductGlueDuration> listData(MesProductGlueDuration mesProductGlueDuration, HttpServletRequest request, HttpServletResponse response) {
+		mesProductGlueDuration.setPage(new Page<>(request, response));
+		Page<MesProductGlueDuration> page = mesProductGlueDurationService.findPage(mesProductGlueDuration);
+		fillOprnoTitle(page.getList());
+		return page;
+	}
+
+	@RequiresPermissions("mes:mesProductGlueDuration:view")
+	@RequestMapping(value = "form")
+	public String form(MesProductGlueDuration mesProductGlueDuration, Model model) {
+		if (StringUtils.isBlank(mesProductGlueDuration.getOprnoTitle())) {
+			mesProductGlueDuration.setOprnoTitle(findOprnoTitle(mesProductGlueDuration.getLineSn(), mesProductGlueDuration.getOprno()));
+		}
+		model.addAttribute("mesProductGlueDuration", mesProductGlueDuration);
+		return "modules/mes/mesProductGlueDurationForm";
+	}
+
+	@RequiresPermissions("mes:mesProductGlueDuration:edit")
+	@PostMapping(value = "save")
+	@ResponseBody
+	public String save(@Validated MesProductGlueDuration mesProductGlueDuration) {
+		mesProductGlueDurationService.save(mesProductGlueDuration);
+		return renderResult(Global.TRUE, text("保存涂胶加工时长成功!"));
+	}
+
+	@RequiresPermissions("mes:mesProductGlueDuration:edit")
+	@RequestMapping(value = "delete")
+	@ResponseBody
+	public String delete(MesProductGlueDuration mesProductGlueDuration) {
+		mesProductGlueDurationService.delete(mesProductGlueDuration);
+		return renderResult(Global.TRUE, text("删除涂胶加工时长成功!"));
+	}
+
+	private void fillOprnoTitle(List<MesProductGlueDuration> list) {
+		if (ListUtils.isEmpty(list)) {
+			return;
+		}
+		Map<String, String> oprnoTitleMap = new HashMap<>();
+		for (MesProductGlueDuration record : list) {
+			if (StringUtils.isNotBlank(record.getOprnoTitle())) {
+				continue;
+			}
+			if (StringUtils.isBlank(record.getLineSn()) || StringUtils.isBlank(record.getOprno())) {
+				record.setOprnoTitle("");
+				continue;
+			}
+			String key = record.getLineSn() + "|" + record.getOprno();
+			if (!oprnoTitleMap.containsKey(key)) {
+				oprnoTitleMap.put(key, findOprnoTitle(record.getLineSn(), record.getOprno()));
+			}
+			record.setOprnoTitle(oprnoTitleMap.get(key));
+		}
+	}
+
+	private String findOprnoTitle(String lineSn, String oprno) {
+		MesLineProcess mesLineProcess = new MesLineProcess();
+		mesLineProcess.setLineSn(lineSn);
+		mesLineProcess.setOprno(oprno);
+		MesLineProcess mesLineProcess1 = mesLineProcessService.findInfo(mesLineProcess);
+		if (!ObjectUtils.isEmpty(mesLineProcess1)) {
+			return mesLineProcess1.getTitle();
+		}
+		String formattedOprno = CommonUitl.formatOprno(oprno);
+		if (StringUtils.equals(oprno, formattedOprno)) {
+			return "";
+		}
+		mesLineProcess.setOprno(formattedOprno);
+		mesLineProcess1 = mesLineProcessService.findInfo(mesLineProcess);
+		return ObjectUtils.isEmpty(mesLineProcess1) ? "" : mesLineProcess1.getTitle();
+	}
+
+}

+ 25 - 6
src/main/java/com/jeesite/modules/mes/web/MesProductQmController.java

@@ -56,14 +56,32 @@ public class MesProductQmController extends BaseController {
 	}
 
 	/**
-	 * 查询列表
+	 * 查询列表(兼容旧菜单,跳转白件气密)
 	 */
 	@RequiresPermissions("mes:mesProductQm:view")
 	@RequestMapping(value = {"list", ""})
 	public String list(MesProductQm mesProductQm, Model model) {
-		/*MesSearchDateReq dateMap = CommonUitl.getSearchDate();
-		mesProductQm.setTestTime_gte(dateMap.getStartDate());
-		mesProductQm.setTestTime_lte(dateMap.getEndtDate());*/
+		return listWhitePart(mesProductQm, model);
+	}
+
+	/**
+	 * 白件气密列表
+	 */
+	@RequiresPermissions("mes:mesProductQm:view")
+	@RequestMapping(value = "listWhitePart")
+	public String listWhitePart(MesProductQm mesProductQm, Model model) {
+		mesProductQm.setQmCategory("whitePart");
+		model.addAttribute("mesProductQm", mesProductQm);
+		return "modules/mes/mesProductQmWhiteList";
+	}
+
+	/**
+	 * 总成气密列表
+	 */
+	@RequiresPermissions("mes:mesProductQm:view")
+	@RequestMapping(value = "listAssembly")
+	public String listAssembly(MesProductQm mesProductQm, Model model) {
+		mesProductQm.setQmCategory("assembly");
 		model.addAttribute("mesProductQm", mesProductQm);
 		return "modules/mes/mesProductQmList";
 	}
@@ -108,8 +126,9 @@ public class MesProductQmController extends BaseController {
 	@RequestMapping(value = "exportData")
 	public void exportData(MesProductQm mesProductQm, HttpServletResponse response) {
 		List<MesProductQm> list = mesProductQmService.findList(mesProductQm);
-		String fileName = "气密检测记录" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
-		try(ExcelExport ee = new ExcelExport("气密检测记录", MesProductQm.class)){
+		String exportTitle = "assembly".equals(mesProductQm.getQmCategory()) ? "总成气密" : "白件气密";
+		String fileName = exportTitle + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+		try(ExcelExport ee = new ExcelExport(exportTitle, MesProductQm.class)){
 			ee.setDataList(list).write(response, fileName);
 		}
 	}

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

@@ -755,6 +755,7 @@ shiro:
     ${adminPath}/mes/mesDevice/screen5Data = anon
     ${adminPath}/mes/mesDevice/oeeScreen = anon
     ${adminPath}/mes/mesDevice/oeeScreenData = anon
+    ${adminPath}/mes/mesDevice/reportState = anon
     ${adminPath}/mes/mesProduct/screenData = anon
     ${adminPath}/mes/mesProduct/screenData4 = anon
     ${adminPath}/mes/mesProduct/screenDataHourly = anon

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

@@ -23,4 +23,13 @@
 		WHERE sn=#{sn}
 		LIMIT 1
 	</select>
+
+	<select id="findByGwAndLineSn" resultType="com.jeesite.modules.mes.entity.MesDevice">
+		SELECT *
+		FROM mes_device a
+		WHERE a.gw = #{gw}
+		  AND a.line_sn = #{lineSn}
+		  AND a.status = '0'
+		LIMIT 1
+	</select>
 </mapper>

+ 15 - 0
src/main/resources/mappings/modules/mes/MesIconsCategoryDao.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.MesIconsCategoryDao">
+
+	<!-- 查询数据
+	<select id="findList" resultType="MesIconsCategory">
+		SELECT ${sqlMap.column.toSql()}
+		FROM ${sqlMap.table.toSql()}
+		<where>
+			${sqlMap.where.toSql()}
+		</where>
+		ORDER BY ${sqlMap.order.toSql()}
+	</select> -->
+
+</mapper>

+ 5 - 0
src/main/resources/mappings/modules/mes/MesProductGlueDurationDao.xml

@@ -0,0 +1,5 @@
+<?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.MesProductGlueDurationDao">
+
+</mapper>

+ 28 - 0
src/main/resources/mappings/modules/mes/MesProductQmDao.xml

@@ -1,6 +1,34 @@
 <?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.MesProductQmDao">
+
+	<sql id="qmCategoryWhere">
+		<if test="qmCategory != null and qmCategory == 'whitePart'">
+			AND (a.title LIKE '%白件%' OR a.title LIKE '%边框%')
+		</if>
+		<if test="qmCategory != null and qmCategory == 'assembly'">
+			AND a.title LIKE '%总成%'
+		</if>
+	</sql>
+
+	<select id="findList" resultType="com.jeesite.modules.mes.entity.MesProductQm" weight="100">
+		SELECT ${sqlMap.column.toSql()}
+		FROM ${sqlMap.table.toSql()}
+		<where>
+			${sqlMap.where.toSql()}
+			<include refid="qmCategoryWhere"/>
+		</where>
+		ORDER BY ${sqlMap.order.toSql()}
+	</select>
+
+	<select id="findCount" resultType="long" weight="100">
+		SELECT COUNT(1)
+		FROM ${sqlMap.table.toSql()}
+		<where>
+			${sqlMap.where.toSql()}
+			<include refid="qmCategoryWhere"/>
+		</where>
+	</select>
 	
 	<!-- 查询数据
 	<select id="findList" resultType="MesProductQm">

+ 0 - 9
src/main/resources/views/modules/mes/erpProductionOrderList.html

@@ -123,15 +123,6 @@ $(document).on('click', '.btn-mo-pushdown', function(e) {
 			<span class="text-muted" style="display:block;margin:4px 0 0 22px;font-size:12px;">${text('选择 HEV/115 分录,勾选 MES 批次码入库')}</span>
 		</a>
 		<% } %>
-		<% if(hasPermi('mes:erpTransferDirect:view')){ %>
-		<a href="javascript:" class="list-group-item mo-pushdown-item"
-				data-url="${ctx}/mes/erpTransferDirect/form?moBillNo="
-				data-title="${text('新增直接调拨单')}">
-			<i class="fa fa-exchange fa-fw text-success"></i>
-			<strong>${text('新增直接调拨单')}</strong>
-			<span class="text-muted" style="display:block;margin:4px 0 0 22px;font-size:12px;">${text('按该订单生产入库批号外发调拨')}</span>
-		</a>
-		<% } %>
 		<% if(hasPermi('mes:erpPickMaterialBill:view')){ %>
 		<a href="javascript:" class="list-group-item mo-pushdown-item"
 				data-url="${ctx}/mes/erpPickMaterialBill/selectDetail?billNo="

+ 0 - 7
src/main/resources/views/modules/mes/erpProductionOrderPushdown.html

@@ -21,13 +21,6 @@
 					<span class="text-muted" style="display:block;margin:4px 0 0 22px;font-size:12px;">${text('选择 HEV/115 分录,勾选 MES 批次码入库')}</span>
 				</a>
 				<% } %>
-				<% if(hasPermi('mes:erpTransferDirect:view')){ %>
-				<a href="${ctx}/mes/erpTransferDirect/form?moBillNo=${billNo!}" class="list-group-item mo-pushdown-link" data-title="${text('新增直接调拨单')} - ${billNo!}">
-					<i class="fa fa-exchange fa-fw text-success"></i>
-					<strong>${text('新增直接调拨单')}</strong>
-					<span class="text-muted" style="display:block;margin:4px 0 0 22px;font-size:12px;">${text('按该订单生产入库批号外发调拨')}</span>
-				</a>
-				<% } %>
 				<% if(hasPermi('mes:erpPickMaterialBill:view')){ %>
 				<a href="${ctx}/mes/erpPickMaterialBill/selectDetail?billNo=${billNo!}" class="list-group-item mo-pushdown-link" data-title="${text('增加领料单')} - ${billNo!}">
 					<i class="fa fa-sign-out fa-fw text-warning"></i>

+ 272 - 104
src/main/resources/views/modules/mes/erpTransferDirectForm.html

@@ -3,73 +3,48 @@
 	<div class="box box-main">
 		<div class="box-header">
 			<div class="box-title">
-				<i class="fa fa-plus"></i> ${text('增加直接调拨单')}:${moBillNo!}
+				<i class="fa fa-plus"></i> ${text('增加直接调拨单')}
 			</div>
 			<div class="box-tools pull-right">
 				<a href="javascript:void(0)" class="btn btn-primary" id="btnSubmitTransfer"><i class="fa fa-check"></i> ${text('提交调拨')}</a>
-				<a href="${ctx}/mes/erpTransferDirect/selectOrder" class="btn btn-default addTabPage" title="${text('返回')}"><i class="fa fa-mail-reply"></i> ${text('返回')}</a>
+				<a href="${ctx}/mes/erpTransferDirect/list" class="btn btn-default addTabPage" title="${text('返回')}"><i class="fa fa-mail-reply"></i> ${text('返回')}</a>
 			</div>
 		</div>
 		<div class="box-body">
-			<% if (isNotBlank(message!)) { %>
-			<div class="alert alert-warning">${message}</div>
-			<% } %>
 			<div class="alert alert-info">
 				<i class="fa fa-info-circle"></i>
-				${text('外发调拨:批号锁定为生产入库批号(与生产订单下查关联)。请选择一行入库明细,填写调拨数量及调出/调入仓库仓位后提交。')}
+				${text('外发调拨:选择 HEV/115 物料,从 ERP 即时库存选择批号,填写调拨数量及调出/调入仓库库位后提交。')}
 			</div>
 			<div class="row" style="margin-bottom:12px;">
-				<div class="col-xs-12 col-sm-6">
-					<label class="control-label">${text('生产订单')}:</label>
-					<span>${moBillNo!}</span>
+				<div class="col-xs-12 col-sm-4">
+					<label class="control-label">${text('调拨类别')}:</label>
+					<p class="form-control-static"><strong>${text('外发调拨')}</strong></p>
 				</div>
 			</div>
-			<div class="table-responsive" style="margin-bottom:16px;">
-				<table class="table table-bordered table-hover table-condensed" id="sourceTable">
-					<thead>
-						<tr>
-							<th style="width:40px;"></th>
-							<th>${text('入库单号')}</th>
-							<th>${text('行号')}</th>
-							<th>${text('物料编码')}</th>
-							<th>${text('物料名称')}</th>
-							<th>${text('批号')}</th>
-							<th>${text('入库数量')}</th>
-							<th>${text('单位')}</th>
-							<th>${text('仓库')}</th>
-						</tr>
-					</thead>
-					<tbody>
-						<% if(sourceList.~size == 0){ %>
-						<tr><td colspan="9" class="text-center text-muted">${text('无可用生产入库行')}</td></tr>
+			<div class="row" style="margin-bottom:12px;">
+				<div class="col-xs-12 col-sm-6">
+					<label class="control-label"><span class="required">*</span> ${text('物料编码')}:</label>
+					<select id="materialNumber" class="form-control" style="max-width:480px;">
+						<option value="">${text('请选择')}</option>
+						<% for(mat in materialNumbers){ %>
+						<% if(mat == '8.10.HEV.10.047696'){ %>
+						<option value="${mat}">${mat}(HEV)</option>
+						<% }else if(mat == '8.35.115.10.5003146600021'){ %>
+						<option value="${mat}">${mat}(115)</option>
 						<% }else{ %>
-						<% for(row in sourceList){ 
-							var instockQtyText = @com.jeesite.modules.mes.erp.ErpKingdeeParseUtils.formatDisplayQty(row.realQty);
-						%>
-						<tr class="source-row" data-entry-id="${row.entryId!}" data-bill-no="${row.billNo!}"
-							data-material="${row.materialNumber!}" data-lot="${row.lotNumber!}"
-							data-qty="${instockQtyText!}" data-stock="${row.stockNumber!}">
-							<td class="text-center">
-								<input type="radio" name="instockRow" class="instock-radio" value="${row.entryId!}"/>
-							</td>
-							<td>${row.billNo!}</td>
-							<td class="text-center">${row.seq!}</td>
-							<td>${row.materialNumber!}</td>
-							<td>${row.materialName!}</td>
-							<td><strong>${row.lotNumber!}</strong></td>
-							<td class="text-right">${instockQtyText!}</td>
-							<td class="text-center">${row.unitName!}</td>
-							<td>${row.stockName!} (${row.stockNumber!})</td>
-						</tr>
+						<option value="${mat}">${mat}</option>
 						<% } %>
 						<% } %>
-					</tbody>
-				</table>
-			</div>
-			<div class="row" style="margin-bottom:12px;">
+					</select>
+				</div>
 				<div class="col-xs-12 col-sm-4">
-					<label class="control-label"><span class="required">*</span> ${text('调拨数量')}:</label>
-					<input type="number" id="transferQty" class="form-control" min="0" step="any" placeholder="${text('不超过所选入库数量')}"/>
+					<label class="control-label"><span class="required">*</span> ${text('单位')}:</label>
+					<select id="unitNumber" class="form-control" style="max-width:220px;">
+						<option value="">${text('请先选择物料')}</option>
+					</select>
+					<input type="text" id="unitNumberManual" class="form-control" style="max-width:220px;margin-top:6px;display:none;"
+						placeholder="${text('手工填写单位编码')}"/>
+					<p class="help-block" id="unitHint"></p>
 				</div>
 			</div>
 			<div class="row" style="margin-bottom:12px;">
@@ -79,14 +54,16 @@
 					<div class="alert alert-warning" style="margin-top:6px;padding:8px 12px;">${text('未能从 ERP 加载仓库列表')}</div>
 					<% }else{ %>
 					<select id="srcStockNumber" class="form-control" style="max-width:420px;">
+						<option value="">${text('请选择')}</option>
 						<% for(stock in stockList){ %>
-						<option value="${stock.stockNumber!}">${stock.displayLabel!}</option>
+						<option value="${stock.stockNumber!}" data-stock-name="${stock.stockName!}"
+							<% if(stock.stockNumber == defaultSrcStockNumber){ %>selected<% } %>>${stock.displayLabel!}</option>
 						<% } %>
 					</select>
 					<% } %>
 				</div>
 				<div class="col-xs-12 col-sm-6">
-					<label class="control-label">${text('调出仓位')}:</label>
+					<label class="control-label">${text('调出库位')}:</label>
 					<select id="srcStockLocNumber" class="form-control" style="max-width:420px;">
 						<option value="">${text('请先选择调出仓库')}</option>
 					</select>
@@ -95,6 +72,24 @@
 			</div>
 			<div class="row" style="margin-bottom:12px;">
 				<div class="col-xs-12 col-sm-6">
+					<label class="control-label"><span class="required">*</span> ${text('批号')}:</label>
+					<input type="hidden" id="lotNumber" value=""/>
+					<input type="hidden" id="lotMaxQty" value=""/>
+					<div class="input-group" style="max-width:480px;">
+						<input type="text" id="lotNumberDisplay" class="form-control" readonly placeholder="${text('请先选择物料和调出仓库,再选批号')}"/>
+						<span class="input-group-btn">
+							<button type="button" class="btn btn-default" id="btnPickLot"><i class="fa fa-search"></i> ${text('选批号')}</button>
+						</span>
+					</div>
+					<p class="help-block" id="lotHint"></p>
+				</div>
+				<div class="col-xs-12 col-sm-4">
+					<label class="control-label"><span class="required">*</span> ${text('调拨数量')}:</label>
+					<input type="number" id="transferQty" class="form-control" min="0" step="any" placeholder="${text('不超过所选批号可用量')}"/>
+				</div>
+			</div>
+			<div class="row" style="margin-bottom:12px;">
+				<div class="col-xs-12 col-sm-6">
 					<label class="control-label"><span class="required">*</span> ${text('调入仓库')}:</label>
 					<% if(stockList.~size == 0){ %>
 					<% }else{ %>
@@ -107,7 +102,7 @@
 					<% } %>
 				</div>
 				<div class="col-xs-12 col-sm-6">
-					<label class="control-label">${text('调入仓位')}:</label>
+					<label class="control-label">${text('调入库位')}:</label>
 					<select id="destStockLocNumber" class="form-control" style="max-width:420px;">
 						<option value="">${text('请先选择调入仓库')}</option>
 					</select>
@@ -117,12 +112,45 @@
 		</div>
 	</div>
 </div>
+<div class="modal fade" id="lotModal" tabindex="-1" role="dialog">
+	<div class="modal-dialog modal-lg" role="document">
+		<div class="modal-content">
+			<div class="modal-header">
+				<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
+				<h4 class="modal-title">${text('选择批号')}</h4>
+			</div>
+			<div class="modal-body">
+				<div class="form-inline" style="margin-bottom:10px;">
+					<div class="form-group">
+						<input type="text" id="lotKeyword" class="form-control input-sm" placeholder="${text('批号关键字')}"/>
+					</div>
+					<button type="button" class="btn btn-primary btn-sm" id="btnLotSearch">${text('搜索')}</button>
+				</div>
+				<p class="help-block" id="lotModalHint"></p>
+				<div class="table-responsive">
+					<table class="table table-bordered table-hover table-condensed" id="lotTable">
+						<thead>
+							<tr>
+								<th style="width:70px;">${text('操作')}</th>
+								<th>${text('批号')}</th>
+								<th style="width:160px;">${text('仓库')}</th>
+								<th style="width:100px;">${text('可用量')}</th>
+							</tr>
+						</thead>
+						<tbody></tbody>
+					</table>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
 <% } %>
 <script>
-var moBillNo = '${moBillNo!}';
 var stockLocUrl = '${ctx}/mes/erpTransferDirect/stockLocOptions';
+var lotOptionsUrl = '${ctx}/mes/erpTransferDirect/lotOptions';
+var lotSearchUrl = '${ctx}/mes/erpTransferDirect/lotSearch';
+var materialInfoUrl = '${ctx}/mes/erpTransferDirect/materialInfo';
 var submitUrl = '${ctx}/mes/erpTransferDirect/submit';
-var defaultSrcStock = '${defaultSrcStockNumber!}';
 
 function unwrapPdaResp(res) {
 	if (res && res.result !== undefined && res.data !== undefined) {
@@ -131,17 +159,82 @@ function unwrapPdaResp(res) {
 	return res;
 }
 
-function getSelectedRow() {
-	var entryId = $('input[name="instockRow"]:checked').val();
-	if (!entryId) return null;
-	var $tr = $('input[name="instockRow"]:checked').closest('tr');
-	return {
-		entryId: entryId,
-		billNo: $tr.data('bill-no'),
-		lotNumber: $tr.data('lot'),
-		maxQty: parseFloat($tr.data('qty')) || 0,
-		stockNumber: $tr.data('stock') || ''
-	};
+function escapeHtml(text) {
+	return String(text == null ? '' : text)
+		.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+}
+
+function clearLotSelection() {
+	$('#lotNumber').val('');
+	$('#lotMaxQty').val('');
+	$('#lotNumberDisplay').val('');
+	$('#lotHint').text('');
+}
+
+function clearUnitSelection() {
+	$('#unitNumber').empty().append('<option value="">${text("请先选择物料")}</option>').show();
+	$('#unitNumberManual').val('').hide();
+	$('#unitHint').text('');
+}
+
+function readUnitNumber() {
+	var manual = $.trim($('#unitNumberManual').val());
+	if (manual) {
+		return manual;
+	}
+	return $.trim($('#unitNumber').val());
+}
+
+function loadMaterialUnits() {
+	var materialNumber = $.trim($('#materialNumber').val());
+	clearUnitSelection();
+	if (!materialNumber) {
+		return;
+	}
+	$('#unitHint').text('${text("加载单位...")}');
+	$.get(materialInfoUrl, { materialNumber: materialNumber }, function(data) {
+		var units = data && data.units ? data.units : [];
+		var $select = $('#unitNumber');
+		$select.empty();
+		if (!units.length) {
+			$select.hide();
+			$('#unitNumberManual').show().focus();
+			$('#unitHint').text(data && data.unitHint ? data.unitHint : '${text("请手工填写单位编码")}');
+			return;
+		}
+		$select.show();
+		$('#unitNumberManual').hide().val('');
+		if (units.length > 1) {
+			$select.append('<option value="">${text("请选择")}</option>');
+		}
+		var defaultUnit = data && data.defaultUnitNumber ? data.defaultUnitNumber : '';
+		$.each(units, function(i, unit) {
+			var number = unit.unitNumber || '';
+			var label = $.trim(unit.unitName || '') || number;
+			var selected = (number === defaultUnit) ? ' selected' : '';
+			$select.append('<option value="' + escapeHtml(number) + '"' + selected + '>' + escapeHtml(label) + '</option>');
+		});
+		$('#unitHint').text(data && data.unitHint ? data.unitHint : '');
+	}, 'json').fail(function() {
+		$('#unitNumber').hide();
+		$('#unitNumberManual').show();
+		$('#unitHint').text('${text("单位加载失败,请手工填写单位编码")}');
+	});
+}
+
+function applyLotSelection(lotNumber, maxQty, hint) {
+	$('#lotNumber').val(lotNumber);
+	$('#lotMaxQty').val(maxQty || '');
+	$('#lotNumberDisplay').val(lotNumber);
+	$('#lotHint').text(hint || ('已选批号 ' + lotNumber));
+	if (maxQty > 0) {
+		$('#transferQty').attr('max', maxQty).val(maxQty);
+	}
+}
+
+function getSrcStockName() {
+	var $opt = $('#srcStockNumber option:selected');
+	return $.trim($opt.attr('data-stock-name') || $opt.text() || '');
 }
 
 function loadStockLoc(stockNumber, $select, $hint, placeholder) {
@@ -156,59 +249,136 @@ function loadStockLoc(stockNumber, $select, $hint, placeholder) {
 		}
 		var defaultLoc = data.defaultLocNumber || '';
 		$.each(data.locations, function(i, loc) {
-			var label = loc.locNumber + (loc.locName ? ' ' + loc.locName : '');
+			var label = $.trim(loc.locName || '') || loc.locNumber;
 			var selected = (loc.locNumber === defaultLoc) ? ' selected' : '';
-			$select.append('<option value="' + loc.locNumber + '" data-lockey="' + (loc.locKey || '') + '"' + selected + '>' + label + '</option>');
+			$select.append('<option value="' + loc.locNumber + '" data-lockey="' + (loc.locKey || '') + '"' + selected + '>' + escapeHtml(label) + '</option>');
 		});
 		$hint.text(data.locHint || '');
 	}, 'json');
 }
 
-function initStockSelection() {
-	var row = getSelectedRow();
-	if (row && row.stockNumber) {
-		$('#srcStockNumber').val(row.stockNumber);
-	}
-	if (defaultSrcStock && $('#srcStockNumber option[value="' + defaultSrcStock + '"]').length) {
-		if (!$('#srcStockNumber').val()) {
-			$('#srcStockNumber').val(defaultSrcStock);
-		}
+function renderLotTable(lots) {
+	var $tbody = $('#lotTable tbody');
+	$tbody.empty();
+	if (!lots || lots.length === 0) {
+		$tbody.append('<tr><td colspan="4" class="text-center text-muted">${text("无可用批号")}</td></tr>');
+		return;
 	}
-	loadStockLoc($('#srcStockNumber').val(), $('#srcStockLocNumber'), $('#srcLocHint'), '${text("无仓位")}');
-	loadStockLoc($('#destStockNumber').val(), $('#destStockLocNumber'), $('#destLocHint'), '${text("无仓位")}');
+	$.each(lots, function(i, lot) {
+		var lotNo = lot.lotNumber || '';
+		var qtyText = lot.qtyText || lot.qty || '0';
+		var maxQty = parseFloat(qtyText) || 0;
+		var stockLabel = (lot.stockNumber || '') + (lot.stockName ? ' ' + lot.stockName : '');
+		var row = '<tr>' +
+			'<td class="text-center"><a href="javascript:void(0)" class="btn-lot-pick" data-lot="' + escapeHtml(lotNo) + '" data-qty="' + maxQty + '">${text("选择")}</a></td>' +
+			'<td><strong>' + escapeHtml(lotNo) + '</strong></td>' +
+			'<td>' + escapeHtml(stockLabel) + '</td>' +
+			'<td class="text-right">' + escapeHtml(qtyText) + '</td>' +
+			'</tr>';
+		$tbody.append(row);
+	});
 }
 
-$(document).on('change', 'input[name="instockRow"]', function() {
-	var row = getSelectedRow();
-	if (row) {
-		$('#transferQty').attr('max', row.maxQty).val(row.maxQty);
-		if (row.stockNumber) {
-			$('#srcStockNumber').val(row.stockNumber);
-			loadStockLoc(row.stockNumber, $('#srcStockLocNumber'), $('#srcLocHint'), '${text("无仓位")}');
+function loadLotOptions(useSearch) {
+	var materialNumber = $.trim($('#materialNumber').val());
+	var stockNumber = $.trim($('#srcStockNumber').val());
+	if (!materialNumber) {
+		js.showMessage('${text("请先选择物料编码")}');
+		return;
+	}
+	if (!stockNumber) {
+		js.showMessage('${text("请先选择调出仓库")}');
+		return;
+	}
+	var pickQty = parseFloat($('#transferQty').val()) || null;
+	var params = {
+		materialNumber: materialNumber,
+		stockNumber: stockNumber,
+		stockName: getSrcStockName()
+	};
+	var url = lotOptionsUrl;
+	if (useSearch) {
+		url = lotSearchUrl;
+		params.lotKeyword = $.trim($('#lotKeyword').val());
+	} else {
+		params.pickQty = pickQty;
+	}
+	$('#lotModalHint').text('${text("加载中...")}');
+	$.get(url, params, function(data) {
+		renderLotTable(data && data.lots ? data.lots : []);
+		$('#lotModalHint').text(data && data.lotHint ? data.lotHint : '');
+		if (!useSearch && data && data.suggestedLotNumber && !$('#lotNumber').val()) {
+			// 仅提示,不自动选中
 		}
+	}, 'json').fail(function() {
+		$('#lotModalHint').text('${text("批号查询失败")}');
+	});
+}
+
+$('#materialNumber, #srcStockNumber').on('change', function() {
+	if ($(this).attr('id') === 'materialNumber') {
+		clearUnitSelection();
+		loadMaterialUnits();
+	}
+	clearLotSelection();
+	if ($('#srcStockNumber').val()) {
+		loadStockLoc($('#srcStockNumber').val(), $('#srcStockLocNumber'), $('#srcLocHint'), '${text("无库位")}');
 	}
 });
 
-$('#srcStockNumber').on('change', function() {
-	loadStockLoc($(this).val(), $('#srcStockLocNumber'), $('#srcLocHint'), '${text("无仓位")}');
-});
 $('#destStockNumber').on('change', function() {
-	loadStockLoc($(this).val(), $('#destStockLocNumber'), $('#destLocHint'), '${text("无仓位")}');
+	loadStockLoc($(this).val(), $('#destStockLocNumber'), $('#destLocHint'), '${text("无库位")}');
 });
 
-$('#btnSubmitTransfer').on('click', function() {
-	var row = getSelectedRow();
-	if (!row) {
-		js.showMessage('${text("请选择一行生产入库明细")}');
+$('#btnPickLot').on('click', function() {
+	if (!$.trim($('#materialNumber').val())) {
+		js.showMessage('${text("请先选择物料编码")}');
+		return;
+	}
+	if (!$.trim($('#srcStockNumber').val())) {
+		js.showMessage('${text("请先选择调出仓库")}');
 		return;
 	}
+	$('#lotKeyword').val('');
+	loadLotOptions(false);
+	$('#lotModal').modal('show');
+});
+
+$('#btnLotSearch').on('click', function() {
+	loadLotOptions(true);
+});
+
+$(document).on('click', '.btn-lot-pick', function() {
+	var lotNumber = $(this).data('lot');
+	var maxQty = parseFloat($(this).data('qty')) || 0;
+	applyLotSelection(lotNumber, maxQty, '已选批号 ' + lotNumber + ',可用 ' + maxQty);
+	$('#lotModal').modal('hide');
+});
+
+$('#btnSubmitTransfer').on('click', function() {
+	var materialNumber = $.trim($('#materialNumber').val());
+	var lotNumber = $.trim($('#lotNumber').val());
+	var unitNumber = readUnitNumber();
 	var qty = parseFloat($('#transferQty').val());
+	var maxQty = parseFloat($('#lotMaxQty').val()) || 0;
+	if (!materialNumber) {
+		js.showMessage('${text("请选择物料编码")}');
+		return;
+	}
+	if (!unitNumber) {
+		js.showMessage('${text("请选择或填写单位")}');
+		return;
+	}
+	if (!lotNumber) {
+		js.showMessage('${text("请选择批号")}');
+		return;
+	}
 	if (!qty || qty <= 0) {
 		js.showMessage('${text("请填写大于 0 的调拨数量")}');
 		return;
 	}
-	if (row.maxQty > 0 && qty > row.maxQty) {
-		js.showMessage('${text("调拨数量不能超过入库数量")} ' + row.maxQty);
+	if (maxQty > 0 && qty > maxQty) {
+		js.showMessage('${text("调拨数量不能超过批号可用量")} ' + maxQty);
 		return;
 	}
 	var srcStock = $.trim($('#srcStockNumber').val());
@@ -226,9 +396,9 @@ $('#btnSubmitTransfer').on('click', function() {
 		return;
 	}
 	var payload = {
-		moBillNo: moBillNo,
-		instockEntryId: row.entryId,
-		instockBillNo: row.billNo,
+		materialNumber: materialNumber,
+		lotNumber: lotNumber,
+		unitNumber: unitNumber,
 		qty: qty,
 		srcStockNumber: srcStock,
 		srcStockLocNumber: $.trim($('#srcStockLocNumber').val()),
@@ -259,9 +429,7 @@ $('#btnSubmitTransfer').on('click', function() {
 });
 
 $(function() {
-	if ($('input[name="instockRow"]').length === 1) {
-		$('input[name="instockRow"]').prop('checked', true).trigger('change');
-	}
-	initStockSelection();
+	loadStockLoc($('#srcStockNumber').val(), $('#srcStockLocNumber'), $('#srcLocHint'), '${text("无库位")}');
+	loadStockLoc($('#destStockNumber').val(), $('#destStockLocNumber'), $('#destLocHint'), '${text("无库位")}');
 });
 </script>

+ 1 - 1
src/main/resources/views/modules/mes/erpTransferDirectList.html

@@ -6,7 +6,7 @@
 				<i class="fa fa-exchange"></i> ${text('ERP直接调拨单查询')}
 			</div>
 			<div class="box-tools pull-right">
-				<a href="${ctx}/mes/erpTransferDirect/selectOrder" class="btn btn-primary btnTool" title="${text('增加直接调拨单')}"><i class="fa fa-plus"></i> ${text('增加')}</a>
+				<a href="${ctx}/mes/erpTransferDirect/form" class="btn btn-primary btnTool" title="${text('增加直接调拨单')}"><i class="fa fa-plus"></i> ${text('增加')}</a>
 				<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>

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

@@ -370,6 +370,12 @@
 			border-color: rgba(255, 176, 122, .9);
 			box-shadow: 0 0 16px rgba(255, 138, 61, .45);
 		}
+		.oee-status-stopped {
+			color: #eef3ff;
+			background: linear-gradient(180deg, #8aa4ff 0%, #6b8cff 100%);
+			border-color: rgba(138, 164, 255, .9);
+			box-shadow: 0 0 16px rgba(107, 140, 255, .45);
+		}
 		.oee-status-offline,
 		.oee-status-unknown {
 			color: #eef3ff;

+ 32 - 1
src/main/resources/views/modules/mes/mesDocumentList.html

@@ -59,6 +59,9 @@
 <% } %>
 <script>
 // 初始化DataGrid对象
+function escapeHtmlAttr(val){
+	return String(val || '').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/'/g,'&#39;').replace(/</g,'&lt;');
+}
 $('#dataGrid').dataGrid({
 	searchForm: $("#searchForm"),
 	columnModel: [
@@ -74,8 +77,11 @@ $('#dataGrid').dataGrid({
 		{header:'${text("创建时间")}', name:'createDate', index:'a.create_date', width:120, align:"center"},
 		{header:'${text("更新人")}', name:'updateBy', index:'a.update_by', width:120, align:"center"},
 		{header:'${text("更新时间")}', name:'updateDate', index:'a.update_date', width:125, align:"center"},
-		{header:'${text("操作")}', name:'actions', width:120, formatter: function(val, obj, row, act){
+		{header:'${text("操作")}', name:'actions', width:150, formatter: function(val, obj, row, act){
 			var actions = [];
+			if (row.previewable && row.previewUrl){
+				actions.push('<a href="javascript:" class="btnPreviewDoc" data-url="'+escapeHtmlAttr(row.previewUrl)+'" data-name="'+escapeHtmlAttr(row.fileName)+'" title="${text("预览")}"><i class="fa fa-eye"></i></a>&nbsp;');
+			}
 			//<% if(hasPermi('mes:mesDocument:edit')){ %>
 				actions.push('<a href="${ctx}/mes/mesDocument/form?id='+row.id+'" class="btnList" title="${text("编辑文档")}"><i class="fa fa-pencil"></i></a>&nbsp;');
 				if (row.status == Global.STATUS_NORMAL){
@@ -94,4 +100,29 @@ $('#dataGrid').dataGrid({
 		
 	}
 });
+// 文档附件在线预览(Word、Excel、PDF)
+$('#dataGrid').on('click', '.btnPreviewDoc', function(){
+	var previewUrl = $(this).data('url');
+	var fileName = $(this).data('name') || '';
+	if (!previewUrl){
+		js.showMessage('${text("暂无可预览的附件")}');
+		return;
+	}
+	if (js.layer){
+		var width = $(js.window).width();
+		width = width < 800 ? width - 30 : width - 200;
+		var height = $(js.window).height();
+		height = height < 500 ? height - 30 : height - 50;
+		js.layer.open({
+			type: 2,
+			maxmin: true,
+			shadeClose: true,
+			title: fileName || '${text("文档预览")}',
+			area: [width + 'px', height + 'px'],
+			content: previewUrl
+		});
+	} else {
+		js.windowOpen(previewUrl);
+	}
+});
 </script>

+ 98 - 0
src/main/resources/views/modules/mes/mesDocumentPreview.html

@@ -0,0 +1,98 @@
+<% layout('/layouts/default.html', {title: '文档预览', libs: []}){ %>
+<style>
+	.doc-preview-wrap { height: calc(100vh - 24px); overflow: hidden; background: #f5f5f5; }
+	.doc-preview-body { height: 100%; overflow: auto; background: #fff; }
+	.doc-preview-body iframe { width: 100%; height: 100%; border: 0; }
+	.doc-preview-body .docx-content { padding: 20px 30px; line-height: 1.6; word-break: break-word; }
+	.doc-preview-body .xlsx-content { padding: 10px; overflow: auto; }
+	.doc-preview-body table { border-collapse: collapse; width: 100%; font-size: 13px; }
+	.doc-preview-body table td, .doc-preview-body table th { border: 1px solid #ddd; padding: 4px 8px; white-space: nowrap; }
+	.doc-preview-tip { text-align: center; padding: 60px 20px; color: #666; }
+	.doc-preview-tip .fa { font-size: 48px; color: #ccc; margin-bottom: 16px; }
+</style>
+<div class="doc-preview-wrap">
+	<div id="previewLoading" class="doc-preview-tip">
+		<i class="fa fa-spinner fa-spin fa-2x"></i>
+		<p style="margin-top:12px;">${text('正在加载预览...')}</p>
+	</div>
+	<div id="previewBody" class="doc-preview-body" style="display:none;">
+		<iframe id="pdfFrame" style="display:none;"></iframe>
+		<div id="docxContent" class="docx-content" style="display:none;"></div>
+		<div id="xlsxContent" class="xlsx-content" style="display:none;"></div>
+	</div>
+	<div id="unsupportedTip" class="doc-preview-tip" style="display:none;">
+		<i class="fa fa-file-o"></i>
+		<p>${text('格式不支持预览,请下载')}</p>
+		<a href="${downloadUrl}" class="btn btn-primary btn-sm" target="_blank"><i class="fa fa-download"></i> ${text('下载文件')}</a>
+	</div>
+	<div id="errorTip" class="doc-preview-tip" style="display:none;">
+		<i class="fa fa-exclamation-circle text-danger"></i>
+		<p id="errorMessage"></p>
+		<a href="${downloadUrl}" class="btn btn-default btn-sm" target="_blank"><i class="fa fa-download"></i> ${text('下载文件')}</a>
+	</div>
+</div>
+<script src="${ctxStatic}/common/preview/mammoth.browser.min.js"></script>
+<script src="${ctxStatic}/common/preview/xlsx.full.min.js"></script>
+<script>
+(function(){
+	var previewType = '${previewType}';
+	var fileUrl = '${fileUrl}';
+	var downloadUrl = '${downloadUrl}';
+
+	function showOnly(id) {
+		$('#previewLoading,#previewBody,#unsupportedTip,#errorTip').hide();
+		$('#' + id).show();
+	}
+	function showError(msg) {
+		$('#errorMessage').text(msg || '${text("预览失败")}');
+		showOnly('errorTip');
+	}
+	function fetchArrayBuffer() {
+		return fetch(fileUrl, { credentials: 'include' }).then(function(resp) {
+			if (!resp.ok) {
+				throw new Error('${text("读取文件失败")}');
+			}
+			return resp.arrayBuffer();
+		});
+	}
+	function renderPdf() {
+		$('#pdfFrame').attr('src', fileUrl).show();
+		showOnly('previewBody');
+	}
+	function renderDocx() {
+		fetchArrayBuffer().then(function(buffer) {
+			return mammoth.convertToHtml({ arrayBuffer: buffer });
+		}).then(function(result) {
+			$('#docxContent').html(result.value).show();
+			showOnly('previewBody');
+		}).catch(function(err) {
+			showError(err.message || '${text("Word 文档解析失败")}');
+		});
+	}
+	function renderXlsx() {
+		fetchArrayBuffer().then(function(buffer) {
+			var workbook = XLSX.read(buffer, { type: 'array' });
+			var sheetName = workbook.SheetNames[0];
+			if (!sheetName) {
+				throw new Error('${text("Excel 文件为空")}');
+			}
+			var html = XLSX.utils.sheet_to_html(workbook.Sheets[sheetName], { editable: false });
+			$('#xlsxContent').html(html).show();
+			showOnly('previewBody');
+		}).catch(function(err) {
+			showError(err.message || '${text("Excel 文档解析失败")}');
+		});
+	}
+
+	if (previewType === 'pdf') {
+		renderPdf();
+	} else if (previewType === 'docx') {
+		renderDocx();
+	} else if (previewType === 'xlsx') {
+		renderXlsx();
+	} else {
+		showOnly('unsupportedTip');
+	}
+})();
+</script>
+<% } %>

+ 78 - 0
src/main/resources/views/modules/mes/mesIconsCategoryForm.html

@@ -0,0 +1,78 @@
+<% 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(mesIconsCategory.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="${mesIconsCategory}" action="${ctx}/mes/mesIconsCategory/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-12">
+						<div class="form-group">
+							<label class="control-label col-sm-2" title="">
+								<span class="required">*</span> ${text('分类名称')}:<i class="fa icon-question hide"></i></label>
+							<div class="col-sm-4">
+								<#form:input path="title" maxlength="100" class="form-control required"/>
+							</div>
+						</div>
+					</div>
+				</div>
+				<div class="row">
+					<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-4">
+								<#form:input path="sortOrder" maxlength="10" class="form-control digits"/>
+								<span class="help-block">${text('数字越小越靠前,默认为 0')}</span>
+							</div>
+						</div>
+					</div>
+				</div>
+				<div class="row">
+					<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-4">
+								<#form:input path="remark" maxlength="255" class="form-control"/>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div class="box-footer">
+				<div class="row">
+					<div class="col-sm-offset-2 col-sm-10">
+						<% if (hasPermi('mes:mesIconsCategory: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>

+ 80 - 0
src/main/resources/views/modules/mes/mesIconsCategoryList.html

@@ -0,0 +1,80 @@
+<% 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:mesIconsCategory:edit')){ %>
+					<a href="${ctx}/mes/mesIconsCategory/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="${mesIconsCategory}" action="${ctx}/mes/mesIconsCategory/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="title" maxlength="100" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('备注')}:</label>
+					<div class="control-inline">
+						<#form:input path="remark" maxlength="255" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('状态')}:</label>
+					<div class="control-inline width-120">
+						<#form:select path="status" dictType="sys_search_status" blankOption="true" class="form-control isQuick"/>
+					</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:'title', index:'a.title', width:150, align:"center", frozen:true, formatter: function(val, obj, row, act){
+			return '<a href="${ctx}/mes/mesIconsCategory/form?id='+row.id+'" class="btnList" data-title="${text("编辑功能模块分类")}">'+(val||row.id)+'</a>';
+		}},
+		{header:'${text("排序")}', name:'sortOrder', index:'a.sort_order', width:80, align:"center"},
+		{header:'${text("备注")}', name:'remark', index:'a.remark', width:200, align:"center"},
+		{header:'${text("状态")}', name:'status', index:'a.status', width:100, align:"center", formatter: function(val, obj, row, act){
+			return js.getDictLabel(${@DictUtils.getDictListJson('sys_search_status')}, val, '${text("未知")}', true);
+		}},
+		{header:'${text("修改时间")}', name:'updateDate', index:'a.update_date', width:150, align:"center"},
+		{header:'${text("操作")}', name:'actions', width:120, formatter: function(val, obj, row, act){
+			var actions = [];
+			//<% if(hasPermi('mes:mesIconsCategory:edit')){ %>
+				actions.push('<a href="${ctx}/mes/mesIconsCategory/form?id='+row.id+'" class="btnList" title="${text("编辑功能模块分类")}"><i class="fa fa-pencil"></i></a>&nbsp;');
+				if (row.id !== '${defaultCategoryId}'){
+					if (row.status == Global.STATUS_NORMAL){
+						actions.push('<a href="${ctx}/mes/mesIconsCategory/disable?id='+row.id+'" class="btnList" title="${text("停用功能模块分类")}" data-confirm="${text("确认要停用该功能模块分类吗?")}"><i class="glyphicon glyphicon-ban-circle"></i></a>&nbsp;');
+					} else if (row.status == Global.STATUS_DISABLE){
+						actions.push('<a href="${ctx}/mes/mesIconsCategory/enable?id='+row.id+'" class="btnList" title="${text("启用功能模块分类")}" data-confirm="${text("确认要启用该功能模块分类吗?")}"><i class="glyphicon glyphicon-ok-circle"></i></a>&nbsp;');
+					}
+					actions.push('<a href="${ctx}/mes/mesIconsCategory/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>

+ 11 - 0
src/main/resources/views/modules/mes/mesIconsForm.html

@@ -28,6 +28,17 @@
 					<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-4">
+								<#form:select path="categoryId" items="${categoryList}" itemLabel="title" itemValue="id" blankOption="true" class="form-control"/>
+							</div>
+						</div>
+					</div>
+				</div>
+				<div class="row">
+					<div class="col-xs-12">
+						<div class="form-group">
+							<label class="control-label col-sm-2" title="">
 								<span class="required">*</span> ${text('标识')}:<i class="fa icon-question hide"></i></label>
 							<div class="col-sm-4">
 								<#form:input path="mode" maxlength="100" class="form-control required"/>

+ 7 - 0
src/main/resources/views/modules/mes/mesIconsList.html

@@ -29,6 +29,12 @@
 					</div>
 				</div>
 				<div class="form-group">
+					<label class="control-label">${text('所属分类')}:</label>
+					<div class="control-inline width-120">
+						<#form:select path="categoryId" items="${categoryList}" itemLabel="title" itemValue="id" blankOption="true" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
 					<label class="control-label">${text('备注')}:</label>
 					<div class="control-inline">
 						<#form:input path="remark" maxlength="255" class="form-control width-120"/>
@@ -65,6 +71,7 @@ $('#dataGrid').dataGrid({
 		{header:'${text("名称")}', name:'title', index:'a.title', width:150, align:"center", frozen:true, formatter: function(val, obj, row, act){
 			return '<a href="${ctx}/mes/mesIcons/form?id='+row.id+'" class="btnList" data-title="${text("编辑功能模块")}">'+(val||row.id)+'</a>';
 		}},
+		{header:'${text("所属分类")}', name:'categoryTitle', index:'a.category_id', width:120, align:"center"},
 		{header:'${text("标识")}', name:'mode', index:'a.mode', width:150, align:"center"},
 		{header:'${text("备注")}', name:'remark', index:'a.remark', width:150, align:"center"},
 		{header:'${text("状态")}', name:'status', index:'a.status', width:150, align:"center", formatter: function(val, obj, row, act){

+ 19 - 15
src/main/resources/views/modules/mes/mesIntegratedScreen.html

@@ -582,8 +582,8 @@
 			</div>
 			<div class="device-split">
 				<div><b id="onlineDevices">0 <small>台</small></b><span>运行设备</span></div>
-				<div><b id="idleDevices">0 <small>台</small></b><span>空闲设备</span></div>
-				<div><b id="offlineDevices">0 <small>台</small></b><span>离线设备</span></div>
+				<div><b id="idleDevices">0 <small>台</small></b><span>停机设备</span></div>
+				<div><b id="offlineDevices">0 <small>台</small></b><span>其他状态</span></div>
 			</div>
 			<div class="device-subtitle">智能设备</div>
 			<div class="ring-row">
@@ -703,10 +703,14 @@
 		var dayPlan = number(data.plan);
 		var finishRate = dayPlan > 0 ? Math.min(100, Math.round(todayOutput * 1000 / dayPlan) / 10) : 0;
 		var deviceTotal = number(data.deviceTotal || data.devices);
-		var onlineDevices = number(data.onlineDevices);
-		var offlineDevices = number(data.offlineDevices);
-		var idleDevices = Math.max(deviceTotal - onlineDevices - offlineDevices, 0);
-		var onlineRate = deviceTotal > 0 ? Math.round(onlineDevices * 1000 / deviceTotal) / 10 : 0;
+		var runningDevices = number(data.runningDevices != null ? data.runningDevices : data.onlineDevices);
+		var stoppedDevices = number(data.stoppedDevices != null ? data.stoppedDevices : data.idleDevices);
+		var otherDevices = number(data.offlineDevices);
+		if (otherDevices <= 0 && deviceTotal > 0) {
+			otherDevices = Math.max(deviceTotal - runningDevices - stoppedDevices, 0);
+		}
+		var avgOpenRate = number(data.avgOpenRate);
+		var runningRate = number(data.runningRate != null ? data.runningRate : (deviceTotal > 0 ? Math.round(runningDevices * 1000 / deviceTotal) / 10 : 0));
 		var todayOut = number(data.todayOut);
 		var monthOut = number(data.monthOut);
 		var scheduleCompleteRate = number(data.scheduleCompleteRate);
@@ -721,15 +725,15 @@
 		$('#monthOut').text(monthOut);
 
 		renderDigits(deviceTotal);
-		$('#onlineDevices').html(onlineDevices + ' <small>台</small>');
-		$('#idleDevices').html(idleDevices + ' <small>台</small>');
-		$('#offlineDevices').html(offlineDevices + ' <small>台</small>');
-		$('#onlineRate').text(onlineRate + '%');
-		$('#performanceRate').text(onlineRate + '%');
-		$('#timeRate').text(finishRate + '%');
-		$('#onlineRateBar').css('width', Math.min(100, onlineRate) + '%');
-		$('#planRateBar').css('width', finishRate + '%');
-		$('#onlineRing').css('background', 'conic-gradient(#3a87ff 0deg, #28d5ff ' + (onlineRate * 3.6) + 'deg, rgba(38, 112, 214, .45) ' + (onlineRate * 3.6) + 'deg)');
+		$('#onlineDevices').html(runningDevices + ' <small>台</small>');
+		$('#idleDevices').html(stoppedDevices + ' <small>台</small>');
+		$('#offlineDevices').html(otherDevices + ' <small>台</small>');
+		$('#onlineRate').text(avgOpenRate + '%');
+		$('#performanceRate').text(runningRate + '%');
+		$('#timeRate').text(avgOpenRate + '%');
+		$('#onlineRateBar').css('width', Math.min(100, runningRate) + '%');
+		$('#planRateBar').css('width', Math.min(100, avgOpenRate) + '%');
+		$('#onlineRing').css('background', 'conic-gradient(#3a87ff 0deg, #28d5ff ' + (avgOpenRate * 3.6) + 'deg, rgba(38, 112, 214, .45) ' + (avgOpenRate * 3.6) + 'deg)');
 
 		renderOrderRows(data.orderDetails || []);
 		renderEnergy(data.greenMeters || [], data);

+ 15 - 66
src/main/resources/views/modules/mes/mesProductArcWeldingList.html

@@ -1,23 +1,20 @@
-<% layout('/layouts/default.html', {title: '点焊参数管理', libs: ['dataGrid']}){ %>
+<% 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('点焊参数管理')}
+				<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="#" class="btn btn-default" id="btnExport"><i class="glyphicon glyphicon-export"></i> 导出</a>
-<!--				<% if(hasPermi('mes:mesProductArcWelding:edit')){ %>-->
-<!--						<a href="#" class="btn btn-default" id="btnImport"><i class="glyphicon glyphicon-import"></i> 导入</a>-->
-<!--					<a href="${ctx}/mes/mesProductArcWelding/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="${mesProductArcWelding}" action="${ctx}/mes/mesProductArcWelding/listData" method="post" class="form-inline hide"
 					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<input type="hidden" name="weldingCategory" value="arc"/>
 				<div class="form-group">
 					<label class="control-label">${text('产线编号')}:</label>
 					<div class="control-inline">
@@ -31,6 +28,12 @@
 					</div>
 				</div>
 				<div class="form-group">
+					<label class="control-label">${text('工位名称')}:</label>
+					<div class="control-inline">
+						<#form:input path="oprnoTitle_like" maxlength="100" class="form-control width-160"/>
+					</div>
+				</div>
+				<div class="form-group">
 					<label class="control-label">${text('二维码')}:</label>
 					<div class="control-inline">
 						<#form:input path="sn" maxlength="50" class="form-control width-120"/>
@@ -48,82 +51,28 @@
 </div>
 <% } %>
 <script>
-// 初始化DataGrid对象
 $('#dataGrid').dataGrid({
 	searchForm: $("#searchForm"),
 	columnModel: [
-		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:150, align:"left"/*, frozen:false, formatter: function(val, obj, row, act){
-			return '<a href="${ctx}/mes/mesProductArcWelding/form?id='+row.id+'&createDate='+encodeURIComponent(row.createDate)+'" 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:"left"},
-		{header:'${text("工位名称")}', name:'oprnoTitle', index:'oprnoTitle', width:150, align:"left", sortable:false},
+		{header:'${text("工位名称")}', name:'oprnoTitle', index:'b.title', width:200, align:"left", sortable:false},
 		{header:'${text("二维码")}', name:'sn', index:'a.sn', width:150, align:"left"},
 		{header:'${text("操作")}', name:'actions', width:120, formatter: function(val, obj, row, act){
 			var actions = [];
-			//<% if(hasPermi('mes:mesProductArcWelding:edit')){ %>
-			// 	actions.push('<a href="${ctx}/mes/mesProductArcWelding/form?id='+row.id+'&createDate='+row.createDate+'" class="btnList" title="${text("编辑点焊参数")}"><i class="fa fa-pencil"></i></a>&nbsp;');
-				// actions.push('<a href="${ctx}/mes/mesProductArcWelding/delete?id='+row.id+'&createDate='+row.createDate+'" class="btnList" title="${text("删除点焊参数")}" data-confirm="${text("确认要删除该点焊参数吗?")}"><i class="fa fa-trash-o"></i></a>&nbsp;');
-			//<% } %>
-				actions.push('<a href="${ctx}/mes/mesProductArcWelding/info?id='+row.id+'&createDate='+encodeURIComponent(row.createDate)+'" class="btnList" title="${text("详情")}"><i class="fa fa-fw fa-eye"></i></a>&nbsp;');
-				return actions.join('');
+			actions.push('<a href="${ctx}/mes/mesProductArcWelding/info?id='+row.id+'&createDate='+encodeURIComponent(row.createDate)+'" class="btnList" title="${text("详情")}"><i class="fa fa-fw fa-eye"></i></a>&nbsp;');
+			return actions.join('');
 		}}
 	],
-	// 加载成功后执行事件
 	ajaxSuccess: function(data){
-		
 	}
 });
-</script><script>
+</script>
+<script>
 $('#btnExport').click(function(){
 	js.ajaxSubmitForm($('#searchForm'), {
 		url:'${ctx}/mes/mesProductArcWelding/exportData',
 		downloadFile:true
 	});
 });
-$('#btnImport').click(function(){
-	js.layer.open({
-		type: 1,
-		area: ['400px'],
-		title: '${text("导入点焊参数")}',
-		resize: false,
-		scrollbar: true,
-		content: js.template('importTpl'),
-		btn: ['<i class="fa fa-check"></i> ${text("导入")}',
-			'<i class="fa fa-remove"></i> ${text("关闭")}'],
-		btn1: function(index, layero){
-			var form = {
-				inputForm: layero.find('#inputForm'),
-				file: layero.find('#file').val()
-			};
-		    if (form.file == '' || (!js.endWith(form.file, '.xls') && !js.endWith(form.file, '.xlsx'))){
-		    	js.showMessage("${text('文件不正确,请选择后缀为“xls”或“xlsx”的文件。')}", null, 'warning');
-		        return false;
-		    }
-			js.ajaxSubmitForm(form.inputForm, function(data){
-				js.showMessage(data.message);
-				if(data.result == Global.TRUE){
-					js.layer.closeAll();
-				}
-				page();
-			}, "json");
-			return true;
-		}
-	});
-});
 </script>
-<script id="importTpl" type="text/template">//<!--
-<form id="inputForm" action="${ctx}/mes/mesProductArcWelding/importData" method="post" enctype="multipart/form-data"
-	class="form-horizontal mt20 mb10" style="overflow:auto;max-height:200px;">
-	<div class="row">
-		<div class="col-xs-12 col-xs-offset-1">
-			<input type="file" id="file" name="file" class="form-file"/>
-			<div class="mt10 pt5" style="color:red">
-				${text('提示:仅允许导入“xls”或“xlsx”格式文件!')}
-			</div>
-			<div class="mt10 pt5">
-				<a href="${ctx}/mes/mesProductArcWelding/importTemplate" class="btn btn-default btn-xs"><i class="fa fa-file-excel-o"></i> ${text('下载模板')}</a>
-			</div>
-		</div>
-	</div>
-</form>
-//--></script>

+ 91 - 0
src/main/resources/views/modules/mes/mesProductGlueDurationForm.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(mesProductGlueDuration.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="${mesProductGlueDuration}" action="${ctx}/mes/mesProductGlueDuration/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="sn" maxlength="64" class="form-control required"/>
+							</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="oprno" maxlength="32" class="form-control required"/>
+							</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="lineSn" maxlength="32" class="form-control required"/>
+							</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="glueDuration" maxlength="50" class="form-control required"/>
+							</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="remarks" rows="3" maxlength="500" class="form-control"/>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div class="box-footer">
+				<div class="row">
+					<div class="col-sm-offset-2 col-sm-10">
+						<% if (hasPermi('mes:mesProductGlueDuration: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>

+ 85 - 0
src/main/resources/views/modules/mes/mesProductGlueDurationList.html

@@ -0,0 +1,85 @@
+<% 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:mesProductGlueDuration:edit')){ %>
+					<a href="${ctx}/mes/mesProductGlueDuration/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="${mesProductGlueDuration}" action="${ctx}/mes/mesProductGlueDuration/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="64" class="form-control width-260"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('工位号')}:</label>
+					<div class="control-inline">
+						<#form:input path="oprno" maxlength="32" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('工位名称')}:</label>
+					<div class="control-inline">
+						<#form:input path="oprnoTitle_like" maxlength="100" class="form-control width-160"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('产线编号')}:</label>
+					<div class="control-inline">
+						<#form:input path="lineSn" maxlength="32" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('涂胶时长')}:</label>
+					<div class="control-inline">
+						<#form:input path="glueDuration" maxlength="50" 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:'sn', index:'a.sn', width:280, align:"left", frozen:true, formatter: function(val, obj, row, act){
+			return '<a href="${ctx}/mes/mesProductGlueDuration/form?id='+row.id+'" class="btnList" data-title="${text("编辑涂胶加工时长")}">'+(val||row.id)+'</a>';
+		}},
+		{header:'${text("工位号")}', name:'oprno', index:'a.oprno', width:120, align:"left"},
+		{header:'${text("工位名称")}', name:'oprnoTitle', index:'b.title', width:180, align:"left", sortable:false},
+		{header:'${text("涂胶时长")}', name:'glueDuration', index:'a.glue_duration', width:120, align:"center"},
+		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:120, align:"left"},
+		{header:'${text("更新时间")}', name:'updateDate', index:'a.update_date', width:150, align:"center"},
+		{header:'${text("操作")}', name:'actions', width:120, formatter: function(val, obj, row, act){
+			var actions = [];
+			//<% if(hasPermi('mes:mesProductGlueDuration:edit')){ %>
+				actions.push('<a href="${ctx}/mes/mesProductGlueDuration/form?id='+row.id+'" class="btnList" title="${text("编辑涂胶加工时长")}"><i class="fa fa-pencil"></i></a>&nbsp;');
+				actions.push('<a href="${ctx}/mes/mesProductGlueDuration/delete?id='+row.id+'" class="btnList" title="${text("删除涂胶加工时长")}" data-confirm="${text("确认要删除该涂胶加工时长记录吗?")}"><i class="fa fa-trash-o"></i></a>&nbsp;');
+			//<% } %>
+			return actions.join('');
+		}}
+	],
+	frozenCols: true,
+	ajaxSuccess: function(data){
+	}
+});
+</script>

+ 15 - 69
src/main/resources/views/modules/mes/mesProductQmList.html

@@ -1,23 +1,20 @@
-<% layout('/layouts/default.html', {title: '气密检测记录管理', libs: ['dataGrid']}){ %>
+<% 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('气密检测记录管理')}
+				<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="#" class="btn btn-default" id="btnExport"><i class="glyphicon glyphicon-export"></i> 导出</a>
-				<% if(hasPermi('mes:mesProductQm:edit')){ %>
-<!--						<a href="#" class="btn btn-default" id="btnImport"><i class="glyphicon glyphicon-import"></i> 导入</a>-->
-<!--					<a href="${ctx}/mes/mesProductQm/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="${mesProductQm}" action="${ctx}/mes/mesProductQm/listData" method="post" class="form-inline hide"
 					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<input type="hidden" name="qmCategory" value="assembly"/>
 				<div class="form-group">
 					<label class="control-label">${text('测试时间')}:</label>
 					<div class="control-inline">
@@ -35,6 +32,12 @@
 					</div>
 				</div>
 				<div class="form-group">
+					<label class="control-label">${text('名称')}:</label>
+					<div class="control-inline">
+						<#form:input path="title_like" maxlength="100" class="form-control width-160"/>
+					</div>
+				</div>
+				<div class="form-group">
 					<label class="control-label">${text('测试结果')}:</label>
 					<div class="control-inline">
 						<#form:input path="testResult" maxlength="200" class="form-control width-120"/>
@@ -43,7 +46,7 @@
 				<div class="form-group">
 					<label class="control-label">${text('产线编号')}:</label>
 					<div class="control-inline">
-						<#form:input path="productCate" maxlength="10" class="form-control width-120"/>
+						<#form:input path="lineSn" maxlength="10" class="form-control width-120"/>
 					</div>
 				</div>
 				<div class="form-group">
@@ -64,88 +67,31 @@
 </div>
 <% } %>
 <script>
-// 初始化DataGrid对象
 $('#dataGrid').dataGrid({
 	searchForm: $("#searchForm"),
 	columnModel: [
 		{header:'${text("工件号")}', name:'componentNum', index:'a.component_num', width:280, align:"center", frozen:true},
-		{header:'${text("产线编号")}', name:'productCate', index:'a.product_cate', width:100, align:"center"},
+		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:100, align:"center"},
 		{header:'${text("工位号")}', name:'oprno', index:'a.oprno', width:100, align:"center"},
-		{header:'${text("名称")}', name:'title', index:'a.title', width:100, align:"center"},
+		{header:'${text("名称")}', name:'title', index:'a.title', width:150, align:"center"},
 		{header:'${text("测试人员")}', name:'workNum', index:'a.work_num', width:150, align:"center"},
 		{header:'${text("测试时间")}', name:'testTime', index:'a.test_time', width:150, align:"center"},
 		{header:'${text("测试压")}', name:'testPressure', index:'a.test_pressure', width:150, align:"center"},
 		{header:'${text("测试压单位")}', name:'testPressureUnit', index:'a.test_pressure_unit', width:150, align:"center"},
 		{header:'${text("泄露值")}', name:'leakVal', index:'a.leak_val', width:150, align:"center"},
 		{header:'${text("泄露值单位")}', name:'leakValUnit', index:'a.leak_val_unit', width:150, align:"center"},
-		// {header:'${text("泄露率")}', name:'leakRate', index:'a.leak_rate', width:150, align:"center"},
-		// {header:'${text("泄露率单位")}', name:'leakRateUnit', index:'a.leak_rate_unit', width:150, align:"center"},
-		{header:'${text("测试结果")}', name:'testResult', index:'a.test_result', width:150, align:"center"},
-		// {header:'${text("操作")}', name:'actions', width:120, formatter: function(val, obj, row, act){
-		// 	var actions = [];
-		// 	//<% if(hasPermi('mes:mesProductQm:edit')){ %>
-		// 		actions.push('<a href="${ctx}/mes/mesProductQm/form?id='+row.id+'" class="btnList" title="${text("编辑气密检测记录")}"><i class="fa fa-pencil"></i></a>&nbsp;');
-		// 	//<% } %>
-		// 	return actions.join('');
-		// }}
+		{header:'${text("测试结果")}', name:'testResult', index:'a.test_result', width:150, align:"center"}
 	],
 	frozenCols: true,
-	// 加载成功后执行事件
 	ajaxSuccess: function(data){
-
 	}
 });
-</script><script>
+</script>
+<script>
 $('#btnExport').click(function(){
 	js.ajaxSubmitForm($('#searchForm'), {
 		url:'${ctx}/mes/mesProductQm/exportData',
 		downloadFile:true
 	});
 });
-$('#btnImport').click(function(){
-	js.layer.open({
-		type: 1,
-		area: ['400px'],
-		title: '${text("导入气密检测记录")}',
-		resize: false,
-		scrollbar: true,
-		content: js.template('importTpl'),
-		btn: ['<i class="fa fa-check"></i> ${text("导入")}',
-			'<i class="fa fa-remove"></i> ${text("关闭")}'],
-		btn1: function(index, layero){
-			var form = {
-				inputForm: layero.find('#inputForm'),
-				file: layero.find('#file').val()
-			};
-		    if (form.file == '' || (!js.endWith(form.file, '.xls') && !js.endWith(form.file, '.xlsx'))){
-		    	js.showMessage("${text('文件不正确,请选择后缀为“xls”或“xlsx”的文件。')}", null, 'warning');
-		        return false;
-		    }
-			js.ajaxSubmitForm(form.inputForm, function(data){
-				js.showMessage(data.message);
-				if(data.result == Global.TRUE){
-					js.layer.closeAll();
-				}
-				page();
-			}, "json");
-			return true;
-		}
-	});
-});
 </script>
-<script id="importTpl" type="text/template">//<!--
-<form id="inputForm" action="${ctx}/mes/mesProductQm/importData" method="post" enctype="multipart/form-data"
-	class="form-horizontal mt20 mb10" style="overflow:auto;max-height:200px;">
-	<div class="row">
-		<div class="col-xs-12 col-xs-offset-1">
-			<input type="file" id="file" name="file" class="form-file"/>
-			<div class="mt10 pt5" style="color:red">
-				${text('提示:仅允许导入“xls”或“xlsx”格式文件!')}
-			</div>
-			<div class="mt10 pt5">
-				<a href="${ctx}/mes/mesProductQm/importTemplate" class="btn btn-default btn-xs"><i class="fa fa-file-excel-o"></i> ${text('下载模板')}</a>
-			</div>
-		</div>
-	</div>
-</form>
-//--></script>

+ 97 - 0
src/main/resources/views/modules/mes/mesProductQmWhiteList.html

@@ -0,0 +1,97 @@
+<% 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="#" class="btn btn-default" id="btnExport"><i class="glyphicon glyphicon-export"></i> 导出</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="${mesProductQm}" action="${ctx}/mes/mesProductQm/listData" method="post" class="form-inline hide"
+					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<input type="hidden" name="qmCategory" value="whitePart"/>
+				<div class="form-group">
+					<label class="control-label">${text('测试时间')}:</label>
+					<div class="control-inline">
+						<#form:input path="testTime_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="testTime_lte.click()"/>
+						&nbsp;-&nbsp;
+						<#form:input path="testTime_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">
+					<label class="control-label">${text('工件号')}:</label>
+					<div class="control-inline">
+						<#form:input path="componentNum" maxlength="200" class="form-control width-260"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('名称')}:</label>
+					<div class="control-inline">
+						<#form:input path="title_like" maxlength="100" class="form-control width-160"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('测试结果')}:</label>
+					<div class="control-inline">
+						<#form:input path="testResult" maxlength="200" 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="10" 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="10" 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:'componentNum', index:'a.component_num', width:280, align:"center", frozen:true},
+		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:100, align:"center"},
+		{header:'${text("工位号")}', name:'oprno', index:'a.oprno', width:100, align:"center"},
+		{header:'${text("名称")}', name:'title', index:'a.title', width:150, align:"center"},
+		{header:'${text("测试人员")}', name:'workNum', index:'a.work_num', width:150, align:"center"},
+		{header:'${text("测试时间")}', name:'testTime', index:'a.test_time', width:150, align:"center"},
+		{header:'${text("测试压")}', name:'testPressure', index:'a.test_pressure', width:150, align:"center"},
+		{header:'${text("测试压单位")}', name:'testPressureUnit', index:'a.test_pressure_unit', width:150, align:"center"},
+		{header:'${text("泄露值")}', name:'leakVal', index:'a.leak_val', width:150, align:"center"},
+		{header:'${text("泄露值单位")}', name:'leakValUnit', index:'a.leak_val_unit', width:150, align:"center"},
+		{header:'${text("测试结果")}', name:'testResult', index:'a.test_result', width:150, align:"center"}
+	],
+	frozenCols: true,
+	ajaxSuccess: function(data){
+	}
+});
+</script>
+<script>
+$('#btnExport').click(function(){
+	js.ajaxSubmitForm($('#searchForm'), {
+		url:'${ctx}/mes/mesProductQm/exportData',
+		downloadFile:true
+	});
+});
+</script>

+ 78 - 0
src/main/resources/views/modules/mes/mesProductSpotWeldingList.html

@@ -0,0 +1,78 @@
+<% 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="#" class="btn btn-default" id="btnExport"><i class="glyphicon glyphicon-export"></i> 导出</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="${mesProductArcWelding}" action="${ctx}/mes/mesProductArcWelding/listData" method="post" class="form-inline hide"
+					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<input type="hidden" name="weldingCategory" value="spot"/>
+				<div class="form-group">
+					<label class="control-label">${text('产线编号')}:</label>
+					<div class="control-inline">
+						<#form:input path="lineSn" maxlength="10" 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="20" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('工位名称')}:</label>
+					<div class="control-inline">
+						<#form:input path="oprnoTitle_like" maxlength="100" class="form-control width-160"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('二维码')}:</label>
+					<div class="control-inline">
+						<#form:input path="sn" maxlength="50" 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:'lineSn', index:'a.line_sn', width:150, align:"left"},
+		{header:'${text("工位号")}', name:'oprno', index:'a.oprno', width:150, align:"left"},
+		{header:'${text("工位名称")}', name:'oprnoTitle', index:'b.title', width:200, align:"left", sortable:false},
+		{header:'${text("二维码")}', name:'sn', index:'a.sn', width:150, align:"left"},
+		{header:'${text("操作")}', name:'actions', width:120, formatter: function(val, obj, row, act){
+			var actions = [];
+			actions.push('<a href="${ctx}/mes/mesProductArcWelding/info?id='+row.id+'&createDate='+encodeURIComponent(row.createDate)+'" class="btnList" title="${text("详情")}"><i class="fa fa-fw fa-eye"></i></a>&nbsp;');
+			return actions.join('');
+		}}
+	],
+	ajaxSuccess: function(data){
+	}
+});
+</script>
+<script>
+$('#btnExport').click(function(){
+	js.ajaxSubmitForm($('#searchForm'), {
+		url:'${ctx}/mes/mesProductArcWelding/exportData',
+		downloadFile:true
+	});
+});
+</script>

+ 17 - 13
src/main/resources/views/modules/mes/mesScreen4.html

@@ -525,8 +525,8 @@
 			</div>
 			<div class="device-split">
 				<div><b id="onlineDevices">0</b><span>运行设备</span></div>
-				<div><b id="idleDevices">0</b><span>空闲设备</span></div>
-				<div><b id="offlineDevices">0</b><span>离线设备</span></div>
+				<div><b id="idleDevices">0</b><span>停机设备</span></div>
+				<div><b id="offlineDevices">0</b><span>其他状态</span></div>
 			</div>
 			<div class="ring-row">
 				<div class="ring" id="onlineRing"><span id="onlineRate">0%</span></div>
@@ -630,10 +630,14 @@
 		var dayPlan = number(data.plan);
 		var finishRate = dayPlan > 0 ? Math.min(100, Math.round(todayOutput * 1000 / dayPlan) / 10) : 0;
 		var deviceTotal = number(data.deviceTotal || data.devices);
-		var onlineDevices = number(data.onlineDevices);
-		var offlineDevices = number(data.offlineDevices);
-		var idleDevices = Math.max(deviceTotal - onlineDevices - offlineDevices, 0);
-		var onlineRate = deviceTotal > 0 ? Math.round(onlineDevices * 1000 / deviceTotal) / 10 : 0;
+		var runningDevices = number(data.runningDevices != null ? data.runningDevices : data.onlineDevices);
+		var stoppedDevices = number(data.stoppedDevices != null ? data.stoppedDevices : data.idleDevices);
+		var otherDevices = number(data.offlineDevices);
+		if (otherDevices <= 0 && deviceTotal > 0) {
+			otherDevices = Math.max(deviceTotal - runningDevices - stoppedDevices, 0);
+		}
+		var avgOpenRate = number(data.avgOpenRate);
+		var runningRate = number(data.runningRate != null ? data.runningRate : (deviceTotal > 0 ? Math.round(runningDevices * 1000 / deviceTotal) / 10 : 0));
 
 		$('#todayOutput').text(todayOutput);
 		$('#dayPlan').text(dayPlan);
@@ -645,13 +649,13 @@
 		$('#checkCount').text(number(data.checkCount));
 
 		renderDigits(deviceTotal);
-		$('#onlineDevices').text(onlineDevices);
-		$('#idleDevices').text(idleDevices);
-		$('#offlineDevices').text(offlineDevices);
-		$('#onlineRate').text(onlineRate + '%');
-		$('#onlineRateBar').css('width', Math.min(100, onlineRate) + '%');
-		$('#planRateBar').css('width', finishRate + '%');
-		$('#onlineRing').css('background', 'conic-gradient(#2af3ff 0deg, #31ffae ' + (onlineRate * 3.6) + 'deg, rgba(24, 82, 130, .72) ' + (onlineRate * 3.6) + 'deg)');
+		$('#onlineDevices').text(runningDevices);
+		$('#idleDevices').text(stoppedDevices);
+		$('#offlineDevices').text(otherDevices);
+		$('#onlineRate').text(avgOpenRate + '%');
+		$('#onlineRateBar').css('width', Math.min(100, runningRate) + '%');
+		$('#planRateBar').css('width', Math.min(100, avgOpenRate) + '%');
+		$('#onlineRing').css('background', 'conic-gradient(#2af3ff 0deg, #31ffae ' + (avgOpenRate * 3.6) + 'deg, rgba(24, 82, 130, .72) ' + (avgOpenRate * 3.6) + 'deg)');
 
 		renderMonitorRows(data.monitorRows || []);
 		renderDeviceRows(data.deviceStatusRows || []);