Sfoglia il codice sorgente

Merge remote-tracking branch 'origin/master'

yutianhao 5 giorni fa
parent
commit
a146d9af55
41 ha cambiato i file con 4688 aggiunte e 972 eliminazioni
  1. 54 0
      db/mysql/mes_screen.sql
  2. 30 0
      db/mysql/mes_screen_manage_menu.sql
  3. 98 0
      db/mysql/mes_work_order.sql
  4. 25 4
      src/main/java/com/jeesite/modules/interceptor/MesClientAccessLogInterceptor.java
  5. 13 0
      src/main/java/com/jeesite/modules/mes/dao/MesScreenDao.java
  6. 27 0
      src/main/java/com/jeesite/modules/mes/dao/MesWorkOrderDao.java
  7. 10 0
      src/main/java/com/jeesite/modules/mes/entity/MesProductRecord.java
  8. 79 0
      src/main/java/com/jeesite/modules/mes/entity/MesScreen.java
  9. 295 0
      src/main/java/com/jeesite/modules/mes/entity/MesWorkOrder.java
  10. 116 0
      src/main/java/com/jeesite/modules/mes/entity/MesWorkOrderSn.java
  11. 11 0
      src/main/java/com/jeesite/modules/mes/service/MesProductRecordService.java
  12. 48 0
      src/main/java/com/jeesite/modules/mes/service/MesScreenService.java
  13. 486 0
      src/main/java/com/jeesite/modules/mes/service/MesWorkOrderService.java
  14. 1 3
      src/main/java/com/jeesite/modules/mes/web/MesMaterialController.java
  15. 344 0
      src/main/java/com/jeesite/modules/mes/web/MesProductController.java
  16. 25 0
      src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java
  17. 77 0
      src/main/java/com/jeesite/modules/mes/web/MesScreenController.java
  18. 188 0
      src/main/java/com/jeesite/modules/mes/web/MesWorkOrderController.java
  19. 8 0
      src/main/java/com/jeesite/modules/utils/ErrorMsg.java
  20. 3 0
      src/main/resources/config/application.yml
  21. 5 0
      src/main/resources/mappings/modules/mes/MesScreenDao.xml
  22. 141 0
      src/main/resources/mappings/modules/mes/MesWorkOrderDao.xml
  23. 274 0
      src/main/resources/static/screen/exec-screen.css
  24. 316 0
      src/main/resources/static/screen/exec-screen.js
  25. 275 0
      src/main/resources/static/screen/kpi-screen.css
  26. 434 0
      src/main/resources/static/screen/kpi-screen.js
  27. 56 0
      src/main/resources/static/screen/screen-fullscreen.js
  28. 17 5
      src/main/resources/views/modules/mes/mesMaterialCheckRecordList.html
  29. 151 0
      src/main/resources/views/modules/mes/mesScreen10.html
  30. 84 0
      src/main/resources/views/modules/mes/mesScreen11.html
  31. 385 960
      src/main/resources/views/modules/mes/mesScreen4.html
  32. 1 0
      src/main/resources/views/modules/mes/mesScreen5.html
  33. 1 0
      src/main/resources/views/modules/mes/mesScreen6.html
  34. 1 0
      src/main/resources/views/modules/mes/mesScreen7.html
  35. 1 0
      src/main/resources/views/modules/mes/mesScreen8.html
  36. 1 0
      src/main/resources/views/modules/mes/mesScreen9.html
  37. 87 0
      src/main/resources/views/modules/mes/mesScreenForm.html
  38. 71 0
      src/main/resources/views/modules/mes/mesScreenList.html
  39. 187 0
      src/main/resources/views/modules/mes/mesWorkOrderForm.html
  40. 190 0
      src/main/resources/views/modules/mes/mesWorkOrderList.html
  41. 72 0
      src/main/resources/views/modules/mes/mesWorkOrderSnList.html

+ 54 - 0
db/mysql/mes_screen.sql

@@ -0,0 +1,54 @@
+-- 大屏目录:名称、地址、说明,供大屏管理新增和删除
+
+CREATE TABLE IF NOT EXISTS `mes_screen` (
+  `id` varchar(64) NOT NULL,
+  `title` varchar(100) DEFAULT '' COMMENT '名称',
+  `screen_url` varchar(255) DEFAULT '' COMMENT '地址',
+  `remark` varchar(255) DEFAULT '' COMMENT '说明',
+  `sort_no` int(11) DEFAULT 30 COMMENT '排序',
+  `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1删除 2停用)',
+  `create_by` varchar(64) DEFAULT NULL,
+  `create_date` datetime DEFAULT NULL,
+  `update_by` varchar(64) DEFAULT NULL,
+  `update_date` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='大屏目录';
+
+INSERT INTO `mes_screen` (`id`, `title`, `screen_url`, `remark`, `sort_no`, `status`, `create_by`, `create_date`, `update_by`, `update_date`)
+SELECT '2099051001', '综合数据大屏', '/mes/mesProduct/screen', '车间总览,看产量、工位和报警。', 10, '0', 'system', NOW(), 'system', NOW()
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `mes_screen` WHERE `id` = '2099051001');
+
+INSERT INTO `mes_screen` (`id`, `title`, `screen_url`, `remark`, `sort_no`, `status`, `create_by`, `create_date`, `update_by`, `update_date`)
+SELECT '2099051002', '生产运营看板', '/mes/mesProduct/screen10', '现场远看。达成率、直通率、OEE、班次报工和分段完工。', 20, '0', 'system', NOW(), 'system', NOW()
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `mes_screen` WHERE `id` = '2099051002');
+
+INSERT INTO `mes_screen` (`id`, `title`, `screen_url`, `remark`, `sort_no`, `status`, `create_by`, `create_date`, `update_by`, `update_date`)
+SELECT '2099051003', '生产简报', '/mes/mesProduct/screen11', '领导近看。今天完成多少、质量是否稳、近七日走势,以及该过问的事项。', 30, '0', 'system', NOW(), 'system', NOW()
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `mes_screen` WHERE `id` = '2099051003');
+
+INSERT INTO `mes_screen` (`id`, `title`, `screen_url`, `remark`, `sort_no`, `status`, `create_by`, `create_date`, `update_by`, `update_date`)
+SELECT '2099051004', '返修区看板', '/mes/mesProduct/screen9', '待返修、待检查、今日完成和返修清单。', 40, '0', 'system', NOW(), 'system', NOW()
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `mes_screen` WHERE `id` = '2099051004');
+
+INSERT INTO `mes_screen` (`id`, `title`, `screen_url`, `remark`, `sort_no`, `status`, `create_by`, `create_date`, `update_by`, `update_date`)
+SELECT '2099051005', 'GP12不良问题点看板', '/mes/mesProduct/screen8', 'GP12 检验项目、判定标准和不良问题点。', 50, '0', 'system', NOW(), 'system', NOW()
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `mes_screen` WHERE `id` = '2099051005');
+
+INSERT INTO `mes_screen` (`id`, `title`, `screen_url`, `remark`, `sort_no`, `status`, `create_by`, `create_date`, `update_by`, `update_date`)
+SELECT '2099051006', '加工记录(OP010-OP100)', '/mes/mesProduct/screen5', '前段工位加工数量,默认工位可在屏上调整。', 60, '0', 'system', NOW(), 'system', NOW()
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `mes_screen` WHERE `id` = '2099051006');
+
+INSERT INTO `mes_screen` (`id`, `title`, `screen_url`, `remark`, `sort_no`, `status`, `create_by`, `create_date`, `update_by`, `update_date`)
+SELECT '2099051007', '加工记录(OP110-OP190)', '/mes/mesProduct/screen6', '中段工位加工数量,默认工位可在屏上调整。', 70, '0', 'system', NOW(), 'system', NOW()
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `mes_screen` WHERE `id` = '2099051007');
+
+INSERT INTO `mes_screen` (`id`, `title`, `screen_url`, `remark`, `sort_no`, `status`, `create_by`, `create_date`, `update_by`, `update_date`)
+SELECT '2099051008', '加工记录(OP200-OP360)', '/mes/mesProduct/screen7', '后段工位加工数量,默认工位可在屏上调整。', 80, '0', 'system', NOW(), 'system', NOW()
+FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `mes_screen` WHERE `id` = '2099051008');
+
+UPDATE `js_sys_menu`
+SET `menu_href` = '/mes/mesScreen/list',
+    `permission` = 'mes:mesScreen:view,mes:mesScreen:edit',
+    `update_by` = 'system',
+    `update_date` = NOW()
+WHERE `menu_code` = '2099051000000000001';

+ 30 - 0
db/mysql/mes_screen_manage_menu.sql

@@ -0,0 +1,30 @@
+-- 生产线管理 / 大屏管理:集中打开各块大屏
+-- 挂在「生产线管理」下,授权给已经能看「MES大屏」的角色
+
+DELETE FROM `js_sys_role_menu` WHERE `menu_code` = '2099051000000000001';
+DELETE FROM `js_sys_menu` WHERE `menu_code` = '2099051000000000001';
+
+INSERT INTO `js_sys_menu`
+(`menu_code`, `parent_code`, `parent_codes`, `tree_sort`, `tree_sorts`, `tree_leaf`, `tree_level`,
+ `tree_names`, `menu_name`, `menu_type`, `menu_href`, `menu_target`, `menu_icon`, `menu_color`, `menu_title`,
+ `permission`, `weight`, `is_show`, `sys_code`, `module_codes`, `component`, `params`, `status`,
+ `create_by`, `create_date`, `update_by`, `update_date`, `remarks`)
+SELECT
+'2099051000000000001', p.`menu_code`, CONCAT(p.`parent_codes`, p.`menu_code`, ','),
+400, CONCAT(p.`tree_sorts`, '0000000400,'), '1', p.`tree_level` + 1,
+CONCAT(p.`tree_names`, '/大屏管理'), '大屏管理', '1',
+'/mes/mesScreen/list', '', 'fa fa-desktop', '', '',
+'mes:mesScreen:view,mes:mesScreen:edit', 40, '1', 'default', 'core', NULL, NULL, '0',
+'system', NOW(), 'system', NOW(), '集中打开各块大屏'
+FROM `js_sys_menu` p
+WHERE p.`menu_code` = '1680784143702568960';
+
+INSERT INTO `js_sys_role_menu` (`role_code`, `menu_code`)
+SELECT rm.`role_code`, '2099051000000000001'
+FROM `js_sys_role_menu` rm
+WHERE rm.`menu_code` = '1915638797464936448'
+  AND NOT EXISTS (
+    SELECT 1 FROM `js_sys_role_menu` x
+    WHERE x.`role_code` = rm.`role_code`
+      AND x.`menu_code` = '2099051000000000001'
+  );

+ 98 - 0
db/mysql/mes_work_order.sql

@@ -0,0 +1,98 @@
+-- 工单:维护和导入。不参与现场上线。
+CREATE TABLE IF NOT EXISTS `mes_work_order` (
+  `id` varchar(64) NOT NULL COMMENT '主键',
+  `work_no` varchar(64) NOT NULL COMMENT '工单号,M开头',
+  `project_code` varchar(64) DEFAULT NULL COMMENT '项目别',
+  `material_sn` varchar(64) NOT NULL COMMENT '总成料号,1开头',
+  `spec_name` varchar(255) DEFAULT NULL COMMENT '规格名称',
+  `drawing_no` varchar(128) DEFAULT NULL COMMENT '图号',
+  `plan_qty` int(11) NOT NULL COMMENT '需生产数量',
+  `open_date` date NOT NULL COMMENT '开单日期',
+  `produce_date` date NOT NULL COMMENT '生产日期',
+  `sale_order_no` varchar(64) DEFAULT NULL COMMENT '订单编号',
+  `line_sn` varchar(64) DEFAULT NULL COMMENT '产线编号,下达前填写',
+  `cate_sn` varchar(64) DEFAULT NULL COMMENT '工件类型编号',
+  `state` char(1) NOT NULL DEFAULT '0' COMMENT '0待下达 1已下达 2生产中 3完工 4暂停 5关闭',
+  `done_qty` int(11) NOT NULL DEFAULT 0 COMMENT '已完工数量,本阶段固定为0',
+  `status` char(1) NOT NULL DEFAULT '0' COMMENT '状态(0正常 1删除 2停用)',
+  `create_by` varchar(64) NOT NULL COMMENT '创建者',
+  `create_date` datetime NOT NULL COMMENT '创建时间',
+  `update_by` varchar(64) NOT NULL COMMENT '更新者',
+  `update_date` datetime NOT NULL COMMENT '更新时间',
+  `remarks` varchar(500) DEFAULT NULL COMMENT '备注信息',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_mes_work_order_no` (`work_no`),
+  KEY `idx_mes_work_order_produce` (`produce_date`),
+  KEY `idx_mes_work_order_state` (`state`),
+  KEY `idx_mes_work_order_material` (`material_sn`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工单';
+
+-- 菜单挂在「生产线管理」下。执行后重新登录刷新菜单。
+DELETE FROM `js_sys_role_menu` WHERE `menu_code` = '2099101000000000001';
+DELETE FROM `js_sys_menu` WHERE `menu_code` = '2099101000000000001';
+
+INSERT INTO `js_sys_menu`
+(`menu_code`, `parent_code`, `parent_codes`, `tree_sort`, `tree_sorts`, `tree_leaf`, `tree_level`,
+ `tree_names`, `menu_name`, `menu_type`, `menu_href`, `menu_target`, `menu_icon`, `menu_color`, `menu_title`,
+ `permission`, `weight`, `is_show`, `sys_code`, `module_codes`, `component`, `params`, `status`,
+ `create_by`, `create_date`, `update_by`, `update_date`, `remarks`)
+SELECT
+'2099101000000000001', p.`menu_code`, CONCAT(p.`parent_codes`, p.`menu_code`, ','),
+180, CONCAT(p.`tree_sorts`, '0000000180,'), '1', p.`tree_level` + 1,
+CONCAT(p.`tree_names`, '/工单管理'), '工单管理', '1',
+'/mes/mesWorkOrder/list', '', 'fa fa-list-alt', '', '',
+'mes:mesWorkOrder:view,mes:mesWorkOrder:edit', 40, '1', 'default', 'core', NULL, NULL, '0',
+'system', NOW(), 'system', NOW(), '工单维护与导入,不影响现场生产'
+FROM `js_sys_menu` p
+WHERE p.`menu_code` = '1680784143702568960';
+
+INSERT INTO `js_sys_role_menu` (`role_code`, `menu_code`)
+SELECT DISTINCT rm.`role_code`, '2099101000000000001'
+FROM `js_sys_role_menu` rm
+INNER JOIN `js_sys_menu` m ON m.`menu_code` = rm.`menu_code`
+WHERE m.`parent_code` = '1680784143702568960'
+  AND NOT EXISTS (
+    SELECT 1 FROM `js_sys_role_menu` x
+    WHERE x.`role_code` = rm.`role_code`
+      AND x.`menu_code` = '2099101000000000001'
+  );
+
+-- 上线工单校验开关。1 生效,0 关闭。在参数设置中修改,修改后重启应用。
+INSERT INTO `js_sys_config` (`id`, `config_name`, `config_key`, `config_value`, `is_sys`, `create_by`, `create_date`, `update_by`, `update_date`, `remarks`)
+SELECT '2099101000000000002', '生产工单-上线校验开关', 'mes.workOrder.check', '1', '0', 'system', NOW(), 'system', NOW(), '1:上线校验工单并回写完工数量;0:不校验、不回写'
+FROM DUAL
+WHERE NOT EXISTS (
+  SELECT 1 FROM `js_sys_config` WHERE `config_key` = 'mes.workOrder.check'
+);
+
+-- 工单工件页。执行后重新登录刷新菜单。
+INSERT INTO `js_sys_menu`
+(`menu_code`, `parent_code`, `parent_codes`, `tree_sort`, `tree_sorts`, `tree_leaf`, `tree_level`,
+ `tree_names`, `menu_name`, `menu_type`, `menu_href`, `menu_target`, `menu_icon`, `menu_color`, `menu_title`,
+ `permission`, `weight`, `is_show`, `sys_code`, `module_codes`, `component`, `params`, `status`,
+ `create_by`, `create_date`, `update_by`, `update_date`, `remarks`)
+SELECT
+'2099101000000000003', p.`menu_code`, CONCAT(p.`parent_codes`, p.`menu_code`, ','),
+190, CONCAT(p.`tree_sorts`, '0000000190,'), '1', p.`tree_level` + 1,
+CONCAT(p.`tree_names`, '/工单工件'), '工单工件', '1',
+'/mes/mesWorkOrder/snList', '', 'fa fa-barcode', '', '',
+'mes:mesWorkOrder:view', 40, '1', 'default', 'core', NULL, NULL, '0',
+'system', NOW(), 'system', NOW(), '查看工单下的工件及状态'
+FROM `js_sys_menu` p
+WHERE p.`menu_code` = '1680784143702568960'
+  AND NOT EXISTS (SELECT 1 FROM `js_sys_menu` WHERE `menu_code` = '2099101000000000003');
+
+INSERT INTO `js_sys_role_menu` (`role_code`, `menu_code`)
+SELECT DISTINCT rm.`role_code`, '2099101000000000003'
+FROM `js_sys_role_menu` rm
+WHERE rm.`menu_code` = '2099101000000000001'
+  AND NOT EXISTS (
+    SELECT 1 FROM `js_sys_role_menu` x
+    WHERE x.`role_code` = rm.`role_code`
+      AND x.`menu_code` = '2099101000000000003'
+  );
+
+-- 上线记录回写工单号。已有库执行一次即可。
+ALTER TABLE `mes_product_record`
+  ADD COLUMN `work_no` varchar(64) DEFAULT NULL COMMENT '工单号' AFTER `line_sn`,
+  ADD INDEX `idx_mes_product_record_work_no` (`work_no`);

+ 25 - 4
src/main/java/com/jeesite/modules/interceptor/MesClientAccessLogInterceptor.java

@@ -19,7 +19,8 @@ import com.jeesite.modules.sys.utils.LogUtils;
 import com.jeesite.modules.sys.utils.UserUtils;
 
 /**
- * 记录工位/客户端等免登录接口访问日志(操作人标记为「未登录客户端」)。
+ * 记录工位/客户端等免登录接口访问日志。
+ * 操作账号优先取请求参数 ucode,没有则标记为 client / 「未登录客户端」。
  */
 @Component
 public class MesClientAccessLogInterceptor implements HandlerInterceptor {
@@ -150,9 +151,7 @@ public class MesClientAccessLogInterceptor implements HandlerInterceptor {
 				start = startObj;
 			}
 			long cost = start > 0 ? (System.currentTimeMillis() - start) : 0L;
-			User client = new User();
-			client.setUserCode(USER_CODE);
-			client.setUserName(USER_NAME);
+			User client = resolveClientUser(request);
 			LogUtils.saveLog(client, request, handler, ex, resolveTitle(request), LOG_TYPE, cost);
 		} catch (Exception e) {
 			logger.debug("记录客户端访问日志失败: {}", e.getMessage());
@@ -161,6 +160,28 @@ public class MesClientAccessLogInterceptor implements HandlerInterceptor {
 		}
 	}
 
+	private User resolveClientUser(HttpServletRequest request) {
+		User client = new User();
+		String ucode = request.getParameter("ucode");
+		if (StringUtils.isBlank(ucode)) {
+			client.setUserCode(USER_CODE);
+			client.setUserName(USER_NAME);
+			return client;
+		}
+		ucode = ucode.trim();
+		client.setUserCode(ucode);
+		client.setUserName(USER_NAME);
+		try {
+			User realUser = UserUtils.get(ucode);
+			if (realUser != null && StringUtils.isNotBlank(realUser.getUserName())) {
+				client.setUserName(realUser.getUserName());
+			}
+		} catch (Exception ignored) {
+			// 免登录请求可能查不到用户,操作人员仍记为「未登录客户端」
+		}
+		return client;
+	}
+
 	private String resolveTitle(HttpServletRequest request) {
 		String uri = request.getRequestURI();
 		if (uri == null) {

+ 13 - 0
src/main/java/com/jeesite/modules/mes/dao/MesScreenDao.java

@@ -0,0 +1,13 @@
+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.MesScreen;
+
+/**
+ * 大屏目录DAO
+ */
+@MyBatisDao
+public interface MesScreenDao extends CrudDao<MesScreen> {
+
+}

+ 27 - 0
src/main/java/com/jeesite/modules/mes/dao/MesWorkOrderDao.java

@@ -0,0 +1,27 @@
+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.MesWorkOrder;
+import com.jeesite.modules.mes.entity.MesWorkOrderSn;
+
+import java.util.List;
+
+/**
+ * 工单DAO接口
+ * @author mes
+ * @version 2026-10-10
+ */
+@MyBatisDao
+public interface MesWorkOrderDao extends CrudDao<MesWorkOrder> {
+
+	MesWorkOrder findByWorkNo(String workNo);
+
+	List<MesWorkOrder> findOnline(String lineSn);
+
+	int countOnlineOk(String workNo);
+
+	int updateDone(MesWorkOrder mesWorkOrder);
+
+	List<MesWorkOrderSn> findSnList(MesWorkOrderSn query);
+}

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

@@ -30,6 +30,7 @@ import java.util.List;
 //		@Column(name="message_type", attrName="messageType", label="报文类型", queryType=QueryType.LIKE),
 		@Column(name="cate_sn", attrName="cateSn", label="工件类型", queryType=QueryType.LIKE),
 		@Column(name="line_sn", attrName="lineSn", label="产线编号", queryType=QueryType.LIKE),
+		@Column(name="work_no", attrName="workNo", label="工单号", queryType=QueryType.EQ),
 		@Column(name="source", attrName="source", label="来源", queryType=QueryType.EQ),
 		@Column(name="materiel_sn", attrName="materielSn", label="物料批次二维码", queryType=QueryType.LIKE),
 		@Column(name="content", attrName="content", label="结果", queryType=QueryType.LIKE),
@@ -78,6 +79,7 @@ public class MesProductRecord extends DataEntity<MesProductRecord> {
 	private Long cdate;
 	private Date updateDate;
 	private String lineSn;
+	private String workNo;
 	private String zcontent;
 	private String ngType;
 	private String processState; // 加工状态
@@ -277,6 +279,14 @@ public class MesProductRecord extends DataEntity<MesProductRecord> {
 		this.lineSn = lineSn;
 	}
 
+	public String getWorkNo() {
+		return workNo;
+	}
+
+	public void setWorkNo(String workNo) {
+		this.workNo = workNo;
+	}
+
 	public Date getUpdateDate_gte() {
 		return sqlMap.getWhere().getValue("update_date", QueryType.GTE);
 	}

+ 79 - 0
src/main/java/com/jeesite/modules/mes/entity/MesScreen.java

@@ -0,0 +1,79 @@
+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;
+
+/**
+ * 大屏目录
+ */
+@Table(name="mes_screen", alias="a", label="大屏信息", columns={
+		@Column(name="id", attrName="id", label="id", isPK=true),
+		@Column(name="title", attrName="title", label="名称", queryType=QueryType.LIKE),
+		@Column(name="screen_url", attrName="screenUrl", label="地址"),
+		@Column(name="remark", attrName="remark", label="说明", queryType=QueryType.LIKE),
+		@Column(name="sort_no", attrName="sortNo", label="排序"),
+		@Column(name="status", attrName="status", label="状态", 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_no ASC, a.update_date DESC"
+)
+public class MesScreen extends DataEntity<MesScreen> {
+
+	private static final long serialVersionUID = 1L;
+	private String title;
+	private String screenUrl;
+	private String remark;
+	private Integer sortNo;
+
+	public MesScreen() {
+		this(null);
+	}
+
+	public MesScreen(String id) {
+		super(id);
+	}
+
+	@NotBlank(message="名称不能为空")
+	@Size(max=100, message="名称长度不能超过 100 个字符")
+	public String getTitle() {
+		return title;
+	}
+
+	public void setTitle(String title) {
+		this.title = title;
+	}
+
+	@NotBlank(message="地址不能为空")
+	@Size(max=255, message="地址长度不能超过 255 个字符")
+	public String getScreenUrl() {
+		return screenUrl;
+	}
+
+	public void setScreenUrl(String screenUrl) {
+		this.screenUrl = screenUrl;
+	}
+
+	@Size(max=255, message="说明长度不能超过 255 个字符")
+	public String getRemark() {
+		return remark;
+	}
+
+	public void setRemark(String remark) {
+		this.remark = remark;
+	}
+
+	public Integer getSortNo() {
+		return sortNo;
+	}
+
+	public void setSortNo(Integer sortNo) {
+		this.sortNo = sortNo;
+	}
+}

+ 295 - 0
src/main/java/com/jeesite/modules/mes/entity/MesWorkOrder.java

@@ -0,0 +1,295 @@
+package com.jeesite.modules.mes.entity;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+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.Table;
+import com.jeesite.common.mybatis.mapper.query.QueryType;
+import com.jeesite.common.utils.excel.annotation.ExcelField;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Align;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Type;
+import com.jeesite.common.utils.excel.annotation.ExcelFields;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+import java.util.Date;
+
+/**
+ * 工单
+ * @author mes
+ * @version 2026-10-10
+ */
+@Table(name="mes_work_order", alias="a", label="工单信息", columns={
+		@Column(name="id", attrName="id", label="id", isPK=true),
+		@Column(name="work_no", attrName="workNo", label="工单号", queryType=QueryType.LIKE),
+		@Column(name="project_code", attrName="projectCode", label="项目别", queryType=QueryType.LIKE),
+		@Column(name="material_sn", attrName="materialSn", label="总成料号", queryType=QueryType.LIKE),
+		@Column(name="spec_name", attrName="specName", label="规格名称", queryType=QueryType.LIKE),
+		@Column(name="drawing_no", attrName="drawingNo", label="图号", queryType=QueryType.LIKE),
+		@Column(name="plan_qty", attrName="planQty", label="需生产数量"),
+		@Column(name="open_date", attrName="openDate", label="开单日期"),
+		@Column(name="produce_date", attrName="produceDate", label="生产日期"),
+		@Column(name="sale_order_no", attrName="saleOrderNo", label="订单编号", queryType=QueryType.LIKE),
+		@Column(name="line_sn", attrName="lineSn", label="产线编号", queryType=QueryType.EQ),
+		@Column(name="cate_sn", attrName="cateSn", label="工件类型", queryType=QueryType.EQ),
+		@Column(name="state", attrName="state", label="状态", queryType=QueryType.EQ),
+		@Column(name="done_qty", attrName="doneQty", label="已完工数量"),
+		@Column(name="status", attrName="status", label="状态", 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),
+		@Column(name="remarks", attrName="remarks", label="备注"),
+	}, joinTable={
+		@JoinTable(type=JoinTable.Type.LEFT_JOIN, entity=MesLine.class, attrName="this", alias="b",
+				on="b.sn = a.line_sn and b.status = '0' and b.id = (select min(l.id) from mes_line l where l.sn = a.line_sn and l.status = '0')",
+				columns={
+						@Column(name="title", attrName="lineTitle", label="产线"),
+				}),
+		@JoinTable(type=JoinTable.Type.LEFT_JOIN, entity=MesProductCate.class, attrName="this", alias="c",
+				on="c.sn = a.cate_sn and c.status = '0'",
+				columns={
+						@Column(name="title", attrName="cateTitle", label="工件类型"),
+				}),
+	}, orderBy="a.produce_date DESC, a.update_date DESC"
+)
+public class MesWorkOrder extends DataEntity<MesWorkOrder> {
+
+	private static final long serialVersionUID = 1L;
+
+	/** 待下达 */
+	public static final String STATE_DRAFT = "0";
+	/** 已下达 */
+	public static final String STATE_ISSUED = "1";
+	/** 生产中,本阶段不写入 */
+	public static final String STATE_PRODUCING = "2";
+	/** 完工,本阶段不写入 */
+	public static final String STATE_DONE = "3";
+	/** 暂停,本阶段不写入 */
+	public static final String STATE_PAUSE = "4";
+	/** 关闭 */
+	public static final String STATE_CLOSED = "5";
+
+	private String workNo;
+	private String projectCode;
+	private String materialSn;
+	private String specName;
+	private String drawingNo;
+	private Integer planQty;
+	private Date openDate;
+	private Date produceDate;
+	private String saleOrderNo;
+	private String lineSn;
+	private String lineTitle;
+	private String cateSn;
+	private String cateTitle;
+	private String state;
+	private Integer doneQty;
+	/** 样表第 1 列,只用于对齐导入列,不入库 */
+	private Integer rowIndex;
+
+	@ExcelFields({
+			@ExcelField(title="序号", attrName="rowIndex", align=Align.CENTER, sort=10),
+			@ExcelField(title="工单号", attrName="workNo", align=Align.CENTER, sort=20),
+			@ExcelField(title="项目别", attrName="projectCode", align=Align.CENTER, sort=30),
+			@ExcelField(title="总成料号", attrName="materialSn", align=Align.CENTER, sort=40),
+			@ExcelField(title="规格名称", attrName="specName", align=Align.CENTER, sort=50),
+			@ExcelField(title="图号", attrName="drawingNo", align=Align.CENTER, sort=60),
+			@ExcelField(title="需生产数量", attrName="planQty", align=Align.CENTER, sort=70),
+			@ExcelField(title="开单日期", attrName="openDate", align=Align.CENTER, sort=80, dataFormat="yyyy-MM-dd"),
+			@ExcelField(title="生产日期", attrName="produceDate", align=Align.CENTER, sort=90, dataFormat="yyyy-MM-dd"),
+			@ExcelField(title="订单编号", attrName="saleOrderNo", align=Align.CENTER, sort=100),
+			@ExcelField(title="状态", attrName="state", align=Align.CENTER, sort=110, type=Type.EXPORT),
+			@ExcelField(title="产线", attrName="lineTitle", align=Align.CENTER, sort=120, type=Type.EXPORT),
+	})
+	public MesWorkOrder() {
+		this(null);
+	}
+
+	public MesWorkOrder(String id) {
+		super(id);
+	}
+
+	@NotBlank(message="工单号不能为空")
+	@Pattern(regexp="M.+", message="工单号必须以M开头")
+	@Size(min=0, max=64, message="工单号长度不能超过 64 个字符")
+	public String getWorkNo() {
+		return workNo;
+	}
+
+	public void setWorkNo(String workNo) {
+		this.workNo = workNo == null ? null : workNo.trim();
+	}
+
+	@Size(min=0, max=64, message="项目别长度不能超过 64 个字符")
+	public String getProjectCode() {
+		return projectCode;
+	}
+
+	public void setProjectCode(String projectCode) {
+		this.projectCode = projectCode == null ? null : projectCode.trim();
+	}
+
+	@NotBlank(message="总成料号不能为空")
+	@Pattern(regexp="1.+", message="总成料号必须以1开头")
+	@Size(min=0, max=64, message="总成料号长度不能超过 64 个字符")
+	public String getMaterialSn() {
+		return materialSn;
+	}
+
+	public void setMaterialSn(String materialSn) {
+		this.materialSn = materialSn == null ? null : materialSn.trim();
+	}
+
+	@Size(min=0, max=255, message="规格名称长度不能超过 255 个字符")
+	public String getSpecName() {
+		return specName;
+	}
+
+	public void setSpecName(String specName) {
+		this.specName = specName == null ? null : specName.trim();
+	}
+
+	@Size(min=0, max=128, message="图号长度不能超过 128 个字符")
+	public String getDrawingNo() {
+		return drawingNo;
+	}
+
+	public void setDrawingNo(String drawingNo) {
+		this.drawingNo = drawingNo == null ? null : drawingNo.trim();
+	}
+
+	@NotNull(message="需生产数量不能为空")
+	@Min(value=1, message="需生产数量必须是大于 0 的整数")
+	public Integer getPlanQty() {
+		return planQty;
+	}
+
+	public void setPlanQty(Integer planQty) {
+		this.planQty = planQty;
+	}
+
+	@JsonFormat(pattern = "yyyy-MM-dd")
+	@NotNull(message="开单日期不能为空")
+	public Date getOpenDate() {
+		return openDate;
+	}
+
+	public void setOpenDate(Date openDate) {
+		this.openDate = openDate;
+	}
+
+	@JsonFormat(pattern = "yyyy-MM-dd")
+	@NotNull(message="生产日期不能为空")
+	public Date getProduceDate() {
+		return produceDate;
+	}
+
+	public void setProduceDate(Date produceDate) {
+		this.produceDate = produceDate;
+	}
+
+	@Size(min=0, max=64, message="订单编号长度不能超过 64 个字符")
+	public String getSaleOrderNo() {
+		return saleOrderNo;
+	}
+
+	public void setSaleOrderNo(String saleOrderNo) {
+		this.saleOrderNo = saleOrderNo == null ? null : saleOrderNo.trim();
+	}
+
+	@Size(min=0, max=64, message="产线编号长度不能超过 64 个字符")
+	public String getLineSn() {
+		return lineSn;
+	}
+
+	public void setLineSn(String lineSn) {
+		this.lineSn = lineSn == null ? null : lineSn.trim();
+	}
+
+	public String getLineTitle() {
+		return lineTitle;
+	}
+
+	public void setLineTitle(String lineTitle) {
+		this.lineTitle = lineTitle;
+	}
+
+	@Size(min=0, max=64, message="工件类型长度不能超过 64 个字符")
+	public String getCateSn() {
+		return cateSn;
+	}
+
+	public void setCateSn(String cateSn) {
+		this.cateSn = cateSn == null ? null : cateSn.trim();
+	}
+
+	public String getCateTitle() {
+		return cateTitle;
+	}
+
+	public void setCateTitle(String cateTitle) {
+		this.cateTitle = cateTitle;
+	}
+
+	@Size(min=0, max=1, message="状态长度不能超过 1 个字符")
+	public String getState() {
+		return state;
+	}
+
+	public void setState(String state) {
+		this.state = state;
+	}
+
+	public Integer getDoneQty() {
+		return doneQty;
+	}
+
+	public void setDoneQty(Integer doneQty) {
+		this.doneQty = doneQty;
+	}
+
+	public Integer getRowIndex() {
+		return rowIndex;
+	}
+
+	public void setRowIndex(Integer rowIndex) {
+		this.rowIndex = rowIndex;
+	}
+
+	public Date getProduceDate_gte() {
+		return sqlMap.getWhere().getValue("produce_date", QueryType.GTE);
+	}
+
+	public void setProduceDate_gte(Date produceDate) {
+		sqlMap.getWhere().and("produce_date", QueryType.GTE, produceDate);
+	}
+
+	public Date getProduceDate_lte() {
+		return sqlMap.getWhere().getValue("produce_date", QueryType.LTE);
+	}
+
+	public void setProduceDate_lte(Date produceDate) {
+		sqlMap.getWhere().and("produce_date", QueryType.LTE, produceDate);
+	}
+
+	public Date getOpenDate_gte() {
+		return sqlMap.getWhere().getValue("open_date", QueryType.GTE);
+	}
+
+	public void setOpenDate_gte(Date openDate) {
+		sqlMap.getWhere().and("open_date", QueryType.GTE, openDate);
+	}
+
+	public Date getOpenDate_lte() {
+		return sqlMap.getWhere().getValue("open_date", QueryType.LTE);
+	}
+
+	public void setOpenDate_lte(Date openDate) {
+		sqlMap.getWhere().and("open_date", QueryType.LTE, openDate);
+	}
+}

+ 116 - 0
src/main/java/com/jeesite/modules/mes/entity/MesWorkOrderSn.java

@@ -0,0 +1,116 @@
+package com.jeesite.modules.mes.entity;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.jeesite.common.entity.DataEntity;
+
+import java.util.Date;
+
+/**
+ * 工单下的工件。数据来自上线记录上的工单号,不单独建表。
+ */
+public class MesWorkOrderSn extends DataEntity<MesWorkOrderSn> {
+
+	private static final long serialVersionUID = 1L;
+
+	private String workNo;
+	private String sn;
+	private String cateSn;
+	private String cateTitle;
+	private String lineSn;
+	private String lineTitle;
+	private Date onlineDate;
+	/** 产品状态:0生产中 1合格 2返修中 3已报废 4待处理 */
+	private String productState;
+	private String currentOprno;
+	private String oprnoTitle;
+	private String stationResult;
+
+	public String getWorkNo() {
+		return workNo;
+	}
+
+	public void setWorkNo(String workNo) {
+		this.workNo = workNo;
+	}
+
+	public String getSn() {
+		return sn;
+	}
+
+	public void setSn(String sn) {
+		this.sn = sn;
+	}
+
+	public String getCateSn() {
+		return cateSn;
+	}
+
+	public void setCateSn(String cateSn) {
+		this.cateSn = cateSn;
+	}
+
+	public String getCateTitle() {
+		return cateTitle;
+	}
+
+	public void setCateTitle(String cateTitle) {
+		this.cateTitle = cateTitle;
+	}
+
+	public String getLineSn() {
+		return lineSn;
+	}
+
+	public void setLineSn(String lineSn) {
+		this.lineSn = lineSn;
+	}
+
+	public String getLineTitle() {
+		return lineTitle;
+	}
+
+	public void setLineTitle(String lineTitle) {
+		this.lineTitle = lineTitle;
+	}
+
+	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+	public Date getOnlineDate() {
+		return onlineDate;
+	}
+
+	public void setOnlineDate(Date onlineDate) {
+		this.onlineDate = onlineDate;
+	}
+
+	public String getProductState() {
+		return productState;
+	}
+
+	public void setProductState(String productState) {
+		this.productState = productState;
+	}
+
+	public String getCurrentOprno() {
+		return currentOprno;
+	}
+
+	public void setCurrentOprno(String currentOprno) {
+		this.currentOprno = currentOprno;
+	}
+
+	public String getOprnoTitle() {
+		return oprnoTitle;
+	}
+
+	public void setOprnoTitle(String oprnoTitle) {
+		this.oprnoTitle = oprnoTitle;
+	}
+
+	public String getStationResult() {
+		return stationResult;
+	}
+
+	public void setStationResult(String stationResult) {
+		this.stationResult = stationResult;
+	}
+}

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

@@ -79,6 +79,9 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 	private MesProductService mesProductService;
 
 	@Autowired
+	private MesWorkOrderService mesWorkOrderService;
+
+	@Autowired
 	private MesProductQualityService mesProductQualityService;
 
 	@Autowired
@@ -329,6 +332,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 			Integer i = 1;
 			Long cdate = Long.valueOf(CommonUitl.Date2TimeStamp(new Date()));
 			List<MesProductRecord> records = new ArrayList<>();
+			boolean workNoMarked = false;
 			for (MesTemplateItems mesTemplateItems : lists){
 				MesProductRecord mpr1 = new MesProductRecord();
 				mpr1.setSn(mesProductRecord.getSn());
@@ -353,6 +357,10 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 							break;
 						}
 					}
+					if(!workNoMarked && "OK".equals(mpr1.getContent()) && StringUtils.isNotBlank(mesProductRecord.getWorkNo())){
+						mpr1.setWorkNo(mesProductRecord.getWorkNo());
+						workNoMarked = true;
+					}
 				}
 
 				records.add(mpr1);
@@ -369,6 +377,9 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 			mesProduct.setState("0");
 			mesProduct.setCate(cateSn);
 			mesProductService.save(mesProduct);
+			if(StringUtils.isNotBlank(mesProductRecord.getWorkNo())){
+				mesWorkOrderService.refreshDoneQty(mesProductRecord.getWorkNo());
+			}
 
 			return "RSOK";
 		}

+ 48 - 0
src/main/java/com/jeesite/modules/mes/service/MesScreenService.java

@@ -0,0 +1,48 @@
+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.MesScreenDao;
+import com.jeesite.modules.mes.entity.MesScreen;
+
+/**
+ * 大屏目录Service
+ */
+@Service
+public class MesScreenService extends CrudService<MesScreenDao, MesScreen> {
+
+	@Override
+	public MesScreen get(MesScreen mesScreen) {
+		return super.get(mesScreen);
+	}
+
+	@Override
+	public Page<MesScreen> findPage(MesScreen mesScreen) {
+		return super.findPage(mesScreen);
+	}
+
+	@Override
+	public List<MesScreen> findList(MesScreen mesScreen) {
+		return super.findList(mesScreen);
+	}
+
+	@Override
+	@Transactional
+	public void save(MesScreen mesScreen) {
+		if (mesScreen.getSortNo() == null) {
+			mesScreen.setSortNo(30);
+		}
+		super.save(mesScreen);
+	}
+
+	@Override
+	@Transactional
+	public void delete(MesScreen mesScreen) {
+		super.delete(mesScreen);
+	}
+}

+ 486 - 0
src/main/java/com/jeesite/modules/mes/service/MesWorkOrderService.java

@@ -0,0 +1,486 @@
+package com.jeesite.modules.mes.service;
+
+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.common.service.ServiceException;
+import com.jeesite.common.utils.excel.ExcelImport;
+import com.jeesite.modules.mes.dao.MesWorkOrderDao;
+import com.jeesite.modules.mes.entity.MesLine;
+import com.jeesite.modules.mes.entity.MesProductCate;
+import com.jeesite.modules.mes.entity.MesWorkOrder;
+import com.jeesite.modules.mes.entity.MesWorkOrderSn;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 工单Service。维护单据,并在上线时校验、回写完工数量。
+ * @author mes
+ * @version 2026-10-10
+ */
+@Service
+public class MesWorkOrderService extends CrudService<MesWorkOrderDao, MesWorkOrder> {
+
+	@Autowired
+	private MesLineService mesLineService;
+
+	@Autowired
+	private MesProductCateService mesProductCateService;
+
+	@Override
+	public MesWorkOrder get(MesWorkOrder mesWorkOrder) {
+		return super.get(mesWorkOrder);
+	}
+
+	@Override
+	public Page<MesWorkOrder> findPage(MesWorkOrder mesWorkOrder) {
+		return super.findPage(mesWorkOrder);
+	}
+
+	@Override
+	public List<MesWorkOrder> findList(MesWorkOrder mesWorkOrder) {
+		return super.findList(mesWorkOrder);
+	}
+
+	/**
+	 * 上线匹配结果。code 为空表示可以生产。
+	 */
+	public static class OnlineMatch {
+		private String code;
+		private MesWorkOrder order;
+
+		public boolean isOk() {
+			return code == null;
+		}
+
+		public String getCode() {
+			return code;
+		}
+
+		public MesWorkOrder getOrder() {
+			return order;
+		}
+	}
+
+	/**
+	 * 参数 mes.workOrder.check 为 1 时,上线校验工单并回写完工数量。
+	 */
+	public boolean isCheckEnabled() {
+		return "1".equals(Global.getConfig("mes.workOrder.check"));
+	}
+
+	/**
+	 * 按产线和工件类型匹配一张已下达或生产中的工单。
+	 * 开关关闭时不校验,order 为空。
+	 */
+	public OnlineMatch matchOnline(String lineSn, String sn, String lineCate) {
+		OnlineMatch match = new OnlineMatch();
+		if (!isCheckEnabled()) {
+			return match;
+		}
+		if (StringUtils.isBlank(lineSn)) {
+			match.code = "WO";
+			return match;
+		}
+		List<MesWorkOrder> orders = dao.findOnline(lineSn.trim());
+		if (orders == null || orders.isEmpty()) {
+			match.code = "WO";
+			return match;
+		}
+		boolean anyCate = false;
+		for (MesWorkOrder order : orders) {
+			if (StringUtils.isNotBlank(order.getCateSn())) {
+				anyCate = true;
+				break;
+			}
+		}
+		if (!anyCate) {
+			match.code = "WC";
+			return match;
+		}
+		String scannedSn = null;
+		if (StringUtils.isNotBlank(sn)) {
+			MesProductCate scanned = mesProductCateService.getCateBySn(sn.trim());
+			if (scanned != null) {
+				scannedSn = scanned.getSn();
+			}
+		}
+		List<MesWorkOrder> matched = new ArrayList<MesWorkOrder>();
+		for (MesWorkOrder order : orders) {
+			if (StringUtils.isBlank(order.getCateSn())) {
+				continue;
+			}
+			if (StringUtils.isNotBlank(scannedSn)) {
+				if (order.getCateSn().equalsIgnoreCase(scannedSn)) {
+					matched.add(order);
+				}
+			} else if (orders.size() == 1 || order.getCateSn().equalsIgnoreCase(lineCate == null ? "" : lineCate)) {
+				matched.add(order);
+			}
+		}
+		if (matched.isEmpty()) {
+			match.code = "WT";
+			return match;
+		}
+		MesWorkOrder order = matched.get(0);
+		int done = order.getDoneQty() == null ? 0 : order.getDoneQty();
+		int plan = order.getPlanQty() == null ? 0 : order.getPlanQty();
+		if (done >= plan) {
+			match.code = "WQ";
+			return match;
+		}
+		match.order = order;
+		return match;
+	}
+
+	@Transactional
+	public void refreshDoneQty(String workNo) {
+		if (StringUtils.isBlank(workNo)) {
+			return;
+		}
+		MesWorkOrder order = findByWorkNo(workNo);
+		if (order == null || !MesWorkOrder.STATUS_NORMAL.equals(order.getStatus())) {
+			return;
+		}
+		if (!MesWorkOrder.STATE_ISSUED.equals(order.getState()) && !MesWorkOrder.STATE_PRODUCING.equals(order.getState())) {
+			return;
+		}
+		int done = dao.countOnlineOk(workNo);
+		order.setDoneQty(done);
+		if (order.getPlanQty() != null && done >= order.getPlanQty()) {
+			order.setState(MesWorkOrder.STATE_DONE);
+		} else if (MesWorkOrder.STATE_ISSUED.equals(order.getState())) {
+			order.setState(MesWorkOrder.STATE_PRODUCING);
+		}
+		dao.updateDone(order);
+	}
+
+	public Page<MesWorkOrderSn> findSnPage(MesWorkOrderSn query) {
+		Page<MesWorkOrderSn> page = query.getPage();
+		page.setList(dao.findSnList(query));
+		return page;
+	}
+
+	public MesWorkOrder findByWorkNo(String workNo) {
+		if (StringUtils.isBlank(workNo)) {
+			return null;
+		}
+		return dao.findByWorkNo(workNo);
+	}
+
+	@Override
+	@Transactional
+	public void save(MesWorkOrder order) {
+		normalize(order);
+		if (order.getIsNewRecord()) {
+			insertFromForm(order);
+			return;
+		}
+		updateFromForm(order);
+	}
+
+	@Override
+	@Transactional
+	public void delete(MesWorkOrder order) {
+		MesWorkOrder old = get(order);
+		if (old == null) {
+			throw new ServiceException(text("工单不存在"));
+		}
+		if (!MesWorkOrder.STATE_DRAFT.equals(old.getState())) {
+			throw new ServiceException(text("只有待下达的工单可以删除"));
+		}
+		super.delete(old);
+	}
+
+	@Transactional
+	public void issue(MesWorkOrder order) {
+		MesWorkOrder old = required(order);
+		if (!MesWorkOrder.STATE_DRAFT.equals(old.getState())) {
+			throw new ServiceException(text("只有待下达的工单可以下达"));
+		}
+		if (StringUtils.isBlank(old.getLineSn())) {
+			throw new ServiceException(text("请先选择产线再下达"));
+		}
+		if (StringUtils.isBlank(old.getCateSn())) {
+			throw new ServiceException(text("请先选择工件类型再下达"));
+		}
+		checkLine(old.getLineSn());
+		checkCate(old.getCateSn());
+		old.setState(MesWorkOrder.STATE_ISSUED);
+		super.save(old);
+	}
+
+	@Transactional
+	public void recall(MesWorkOrder order) {
+		MesWorkOrder old = required(order);
+		if (!MesWorkOrder.STATE_ISSUED.equals(old.getState())) {
+			throw new ServiceException(text("只有已下达的工单可以撤回"));
+		}
+		old.setState(MesWorkOrder.STATE_DRAFT);
+		super.save(old);
+	}
+
+	@Transactional
+	public void close(MesWorkOrder order) {
+		MesWorkOrder old = required(order);
+		if (!MesWorkOrder.STATE_DRAFT.equals(old.getState()) && !MesWorkOrder.STATE_ISSUED.equals(old.getState())) {
+			throw new ServiceException(text("当前状态不能关闭"));
+		}
+		old.setState(MesWorkOrder.STATE_CLOSED);
+		super.save(old);
+	}
+
+	/**
+	 * 样表第 1 行是说明,第 2 行是表头,数据从第 3 行开始。
+	 */
+	@Transactional
+	public String importData(MultipartFile file) {
+		if (file == null) {
+			throw new ServiceException(text("请选择导入的数据文件!"));
+		}
+		int successNum = 0;
+		int failureNum = 0;
+		StringBuilder successMsg = new StringBuilder();
+		StringBuilder failureMsg = new StringBuilder();
+		Set<String> seen = new HashSet<String>();
+		try (ExcelImport ei = new ExcelImport(file, 2, 0)) {
+			List<MesWorkOrder> list = ei.getDataList(MesWorkOrder.class);
+			for (MesWorkOrder order : list) {
+				normalize(order);
+				if (isBlankRow(order)) {
+					continue;
+				}
+				String label = StringUtils.defaultIfBlank(order.getWorkNo(), "空工单号");
+				try {
+					if (StringUtils.isNotBlank(order.getWorkNo()) && !seen.add(order.getWorkNo())) {
+						throw new ServiceException(text("文件中工单号重复"));
+					}
+					saveImported(order);
+					successNum++;
+					successMsg.append("<br/>").append(successNum).append("、工单 ").append(label).append(" 导入成功");
+				} catch (Exception e) {
+					failureNum++;
+					failureMsg.append("<br/>").append(failureNum).append("、工单 ").append(label).append(" 导入失败:")
+							.append(e.getMessage());
+					logger.error(e.getMessage(), e);
+				}
+			}
+		} catch (ServiceException e) {
+			throw e;
+		} catch (Exception e) {
+			logger.error(e.getMessage(), e);
+			throw new ServiceException(e.getMessage());
+		}
+		if (successNum == 0 && failureNum == 0) {
+			throw new ServiceException(text("没有可导入的数据"));
+		}
+		if (failureNum > 0) {
+			failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据不正确,错误如下:");
+			throw new ServiceException(failureMsg.toString());
+		}
+		successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
+		return successMsg.toString();
+	}
+
+	private void insertFromForm(MesWorkOrder order) {
+		validateCore(order);
+		checkLine(order.getLineSn());
+		checkCate(order.getCateSn());
+		MesWorkOrder exist = findByWorkNo(order.getWorkNo());
+		if (exist != null && !MesWorkOrder.STATUS_DELETE.equals(exist.getStatus())) {
+			throw new ServiceException(text("工单号已存在"));
+		}
+		if (exist != null) {
+			revive(exist, order, true);
+			return;
+		}
+		order.setState(MesWorkOrder.STATE_DRAFT);
+		order.setDoneQty(0);
+		super.save(order);
+	}
+
+	private void updateFromForm(MesWorkOrder order) {
+		MesWorkOrder old = get(order);
+		if (old == null) {
+			throw new ServiceException(text("工单不存在"));
+		}
+		if (!StringUtils.equals(old.getWorkNo(), order.getWorkNo())) {
+			throw new ServiceException(text("工单号不能修改"));
+		}
+		if (MesWorkOrder.STATE_DRAFT.equals(old.getState())) {
+			validateCore(order);
+			checkLine(order.getLineSn());
+			checkCate(order.getCateSn());
+			order.setState(old.getState());
+			order.setDoneQty(old.getDoneQty() == null ? 0 : old.getDoneQty());
+			super.save(order);
+			return;
+		}
+		if (MesWorkOrder.STATE_ISSUED.equals(old.getState())) {
+			if (StringUtils.isBlank(order.getLineSn())) {
+				throw new ServiceException(text("已下达工单不能清空产线"));
+			}
+			checkLine(order.getLineSn());
+			if (order.getOpenDate() == null || order.getProduceDate() == null) {
+				throw new ServiceException(text("开单日期和生产日期不能为空"));
+			}
+			old.setLineSn(order.getLineSn());
+			old.setOpenDate(order.getOpenDate());
+			old.setProduceDate(order.getProduceDate());
+			old.setSaleOrderNo(order.getSaleOrderNo());
+			old.setRemarks(order.getRemarks());
+			super.save(old);
+			return;
+		}
+		throw new ServiceException(text("当前状态不能修改"));
+	}
+
+	private void saveImported(MesWorkOrder incoming) {
+		validateCore(incoming);
+		MesWorkOrder exist = findByWorkNo(incoming.getWorkNo());
+		if (exist == null) {
+			incoming.setIsNewRecord(true);
+			incoming.setState(MesWorkOrder.STATE_DRAFT);
+			incoming.setDoneQty(0);
+			incoming.setLineSn(null);
+			super.save(incoming);
+			return;
+		}
+		if (MesWorkOrder.STATUS_DELETE.equals(exist.getStatus())) {
+			revive(exist, incoming, false);
+			return;
+		}
+		if (!MesWorkOrder.STATE_DRAFT.equals(exist.getState())) {
+			throw new ServiceException(text("不是待下达,不能通过导入覆盖"));
+		}
+		copySchedule(exist, incoming);
+		super.save(exist);
+	}
+
+	private void revive(MesWorkOrder exist, MesWorkOrder incoming, boolean fromForm) {
+		exist.setStatus(MesWorkOrder.STATUS_NORMAL);
+		updateStatus(exist);
+		if (fromForm) {
+			copySchedule(exist, incoming);
+			exist.setLineSn(incoming.getLineSn());
+			exist.setRemarks(incoming.getRemarks());
+		} else {
+			copySchedule(exist, incoming);
+			exist.setLineSn(null);
+			exist.setRemarks(null);
+		}
+		exist.setState(MesWorkOrder.STATE_DRAFT);
+		exist.setDoneQty(0);
+		super.save(exist);
+	}
+
+	private void copySchedule(MesWorkOrder target, MesWorkOrder source) {
+		target.setProjectCode(source.getProjectCode());
+		target.setMaterialSn(source.getMaterialSn());
+		target.setSpecName(source.getSpecName());
+		target.setDrawingNo(source.getDrawingNo());
+		target.setPlanQty(source.getPlanQty());
+		target.setOpenDate(source.getOpenDate());
+		target.setProduceDate(source.getProduceDate());
+		target.setSaleOrderNo(source.getSaleOrderNo());
+	}
+
+	private MesWorkOrder required(MesWorkOrder order) {
+		MesWorkOrder old = get(order);
+		if (old == null) {
+			throw new ServiceException(text("工单不存在"));
+		}
+		return old;
+	}
+
+	private void validateCore(MesWorkOrder order) {
+		if (StringUtils.isBlank(order.getWorkNo()) || !order.getWorkNo().matches("M.+")) {
+			throw new ServiceException(text("工单号必须以M开头"));
+		}
+		if (order.getWorkNo().length() > 64) {
+			throw new ServiceException(text("工单号长度不能超过 64 个字符"));
+		}
+		if (StringUtils.isBlank(order.getMaterialSn()) || !order.getMaterialSn().matches("1.+")) {
+			throw new ServiceException(text("总成料号必须以1开头"));
+		}
+		if (order.getMaterialSn().length() > 64) {
+			throw new ServiceException(text("总成料号长度不能超过 64 个字符"));
+		}
+		if (order.getPlanQty() == null || order.getPlanQty() < 1) {
+			throw new ServiceException(text("需生产数量必须是大于 0 的整数"));
+		}
+		if (order.getOpenDate() == null) {
+			throw new ServiceException(text("开单日期不能为空"));
+		}
+		if (order.getProduceDate() == null) {
+			throw new ServiceException(text("生产日期不能为空"));
+		}
+	}
+
+	private void checkLine(String lineSn) {
+		if (StringUtils.isBlank(lineSn)) {
+			return;
+		}
+		MesLine query = new MesLine();
+		query.setSn(lineSn);
+		query.setStatus(MesLine.STATUS_NORMAL);
+		MesLine line = mesLineService.findInfo(query);
+		if (line == null) {
+			throw new ServiceException(text("产线不存在:" + lineSn));
+		}
+	}
+
+	private void checkCate(String cateSn) {
+		if (StringUtils.isBlank(cateSn)) {
+			return;
+		}
+		MesProductCate cate = mesProductCateService.findBySn(cateSn);
+		if (cate == null || !MesProductCate.STATUS_NORMAL.equals(cate.getStatus())) {
+			throw new ServiceException(text("工件类型不存在:" + cateSn));
+		}
+	}
+
+	private void normalize(MesWorkOrder order) {
+		order.setWorkNo(plain(order.getWorkNo()));
+		order.setProjectCode(plain(order.getProjectCode()));
+		order.setMaterialSn(plain(order.getMaterialSn()));
+		order.setDrawingNo(plain(order.getDrawingNo()));
+		order.setSaleOrderNo(plain(order.getSaleOrderNo()));
+		order.setLineSn(plain(order.getLineSn()));
+		order.setCateSn(plain(order.getCateSn()));
+		if (order.getSpecName() != null) {
+			order.setSpecName(order.getSpecName().trim());
+		}
+	}
+
+	private String plain(String value) {
+		if (value == null) {
+			return null;
+		}
+		String text = value.trim();
+		if (text.matches("\\d+\\.0+")) {
+			return text.substring(0, text.indexOf('.'));
+		}
+		return text;
+	}
+
+	private boolean isBlankRow(MesWorkOrder order) {
+		return StringUtils.isBlank(order.getWorkNo())
+				&& StringUtils.isBlank(order.getProjectCode())
+				&& StringUtils.isBlank(order.getMaterialSn())
+				&& StringUtils.isBlank(order.getSpecName())
+				&& StringUtils.isBlank(order.getDrawingNo())
+				&& (order.getPlanQty() == null || order.getPlanQty() == 0)
+				&& order.getOpenDate() == null
+				&& order.getProduceDate() == null
+				&& StringUtils.isBlank(order.getSaleOrderNo());
+	}
+}

+ 1 - 3
src/main/java/com/jeesite/modules/mes/web/MesMaterialController.java

@@ -8,8 +8,6 @@ import com.alibaba.fastjson.JSONValidator;
 import com.jeesite.common.codec.EncodeUtils;
 import com.jeesite.common.lang.DateUtils;
 import com.jeesite.common.utils.excel.ExcelExport;
-import com.jeesite.modules.mes.entity.MesDevicePrevent;
-import com.jeesite.modules.mes.entity.MesNg;
 import org.apache.shiro.authz.annotation.RequiresPermissions;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
@@ -132,7 +130,7 @@ public class MesMaterialController extends BaseController {
 	public void exportData(MesMaterial mesMaterial, HttpServletResponse response) {
 		List<MesMaterial> list = mesMaterialService.findList(mesMaterial);
 		String fileName = "物料表" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
-		try(ExcelExport ee = new ExcelExport("物料表", MesNg.class)){
+		try(ExcelExport ee = new ExcelExport("物料表", MesMaterial.class)){
 			ee.setDataList(list).write(response, fileName);
 		}
 	}

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

@@ -4,6 +4,7 @@ import java.text.DateFormat;
 import java.text.DecimalFormat;
 import java.text.SimpleDateFormat;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
 import javax.servlet.http.HttpServletRequest;
@@ -91,6 +92,8 @@ public class MesProductController extends BaseController {
 	private MesRepairService mesRepairService;
 	@Autowired
 	private FileUploadService fileUploadService;
+	@Autowired
+	private MesPlanService mesPlanService;
 
 
 	/**
@@ -330,6 +333,21 @@ public class MesProductController extends BaseController {
 		return "modules/mes/mesScreen9";
 	}
 
+	@RequestMapping(value = "screen10")
+	public String screen10(Model model) {
+		return "modules/mes/mesScreen10";
+	}
+
+	@RequestMapping(value = "screen11")
+	public String screen11(Model model) {
+		return "modules/mes/mesScreen11";
+	}
+
+	@RequestMapping(value = "screenManage")
+	public String screenManage() {
+		return REDIRECT + adminPath + "/mes/mesScreen/list";
+	}
+
 	/**
 	 * 返修区大屏数据:汇总统计 + 待处理返修清单(数据来源 mes_repair)
 	 * state: 1=待返修 2=待检查 3=已完成 4=返修NG待处理
@@ -978,4 +996,330 @@ public class MesProductController extends BaseController {
 		resp.setData(map);
 		return resp;
 	}
+
+	/**
+	 * 生产运营看板:达成率、直通率、OEE、班次报工。
+	 * 统计日为当天 08:00 至次日 08:00;8 点前归入上一统计日。
+	 * 达成率 = 完工 / 班次计划;直通率 = 工位合格 / (合格 + 不良);
+	 * OEE = 时间稼动 × 性能稼动 × 合格率。时间稼动按未关闭报警占在用工位的比例折算。
+	 */
+	@RequestMapping(value = "screenKpiData")
+	@ResponseBody
+	public CommonResp screenKpiData() {
+		CommonResp<Map<String, Object>> resp = new CommonResp<>();
+		LocalDateTime now = LocalDateTime.now();
+		LocalDate day = now.getHour() < 8 ? now.toLocalDate().minusDays(1) : now.toLocalDate();
+		String dayStr = day.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+		String dayStart = dayStr + " 08:00:00";
+		String dayEnd = day.plusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + " 08:00:00";
+
+		List<MesPlan> todayPlans = loadPlans(dayStr);
+		long plan = sumPlanNums(todayPlans, null);
+		long actual = countFinished(dayStart, dayEnd, null);
+		long repair = countRepair(dayStart, dayEnd);
+		long ok = countCraftResult("OK", dayStart, dayEnd);
+		long ng = countCraftResult("NG", dayStart, dayEnd);
+		double achieve = ratio(actual, plan, false);
+		double fpy = firstPassRate(ok, ng, actual, repair);
+		double performance = plan <= 0 ? 0D : ratio(actual, plan, true);
+		long openAlarm = countOpenAlarm();
+		long stations = countStations();
+		double availability = availabilityRate(openAlarm, stations);
+		double oee = round1(availability * performance * fpy / 10000D);
+
+		MesShiftDataResp curShift = mesShiftService.getCurShift();
+		String curShiftId = curShift == null || curShift.getMesShift() == null ? "" : curShift.getMesShift().getId();
+		String curShiftTitle = curShift == null || curShift.getMesShift() == null ? "未排班" : curShift.getMesShift().getTitle();
+
+		MesShift shiftQuery = new MesShift();
+		shiftQuery.setStatus("0");
+		List<MesShift> shifts = mesShiftService.findList(shiftQuery);
+		if (shifts == null) {
+			shifts = ListUtils.newArrayList();
+		}
+		shifts.sort(Comparator.comparing(item -> hourMinute(item.getStart())));
+
+		List<Map<String, Object>> shiftRows = ListUtils.newArrayList();
+		double curShiftAchieve = 0D;
+		for (MesShift shift : shifts) {
+			String[] window = shiftWindow(day, shift);
+			if (window == null) {
+				continue;
+			}
+			long shiftPlan = sumPlanNums(todayPlans, shift.getId());
+			long shiftActual = countFinished(window[0], window[1], null);
+			long shiftOk = countCraftResult("OK", window[0], window[1]);
+			long shiftNg = countCraftResult("NG", window[0], window[1]);
+			long shiftRepair = countRepair(window[0], window[1]);
+			double shiftAchieve = ratio(shiftActual, shiftPlan, false);
+			double shiftFpy = firstPassRate(shiftOk, shiftNg, shiftActual, shiftRepair);
+			boolean current = StringUtils.equals(curShiftId, shift.getId());
+			if (current) {
+				curShiftAchieve = shiftAchieve;
+			}
+			Map<String, Object> row = MapUtils.newHashMap();
+			row.put("title", shift.getTitle());
+			row.put("range", window[2]);
+			row.put("plan", shiftPlan);
+			row.put("actual", shiftActual);
+			row.put("ok", shiftOk);
+			row.put("ng", shiftNg);
+			row.put("repair", shiftRepair);
+			row.put("achieve", shiftAchieve);
+			row.put("fpy", shiftFpy);
+			row.put("current", current);
+			shiftRows.add(row);
+		}
+
+		List<Map<String, Object>> productRows = buildProductRows(todayPlans, dayStart, dayEnd);
+		List<String> hourLabels = ListUtils.newArrayList();
+		List<Long> hourActual = ListUtils.newArrayList();
+		List<Long> hourPlan = ListUtils.newArrayList();
+		long hourTarget = plan <= 0 ? 0 : Math.round(plan / 12D);
+		LocalDateTime bucket = day.atTime(8, 0);
+		DateTimeFormatter hourFmt = DateTimeFormatter.ofPattern("HH:mm");
+		DateTimeFormatter fullFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+		for (int i = 0; i < 12; i++) {
+			LocalDateTime bucketEnd = bucket.plusHours(2);
+			hourLabels.add(bucket.format(hourFmt));
+			hourActual.add(countFinished(bucket.format(fullFmt), bucketEnd.format(fullFmt), null));
+			hourPlan.add(hourTarget);
+			bucket = bucketEnd;
+		}
+
+		List<String> trendLabels = ListUtils.newArrayList();
+		List<Double> trendAchieve = ListUtils.newArrayList();
+		List<Double> trendFpy = ListUtils.newArrayList();
+		List<Double> trendOee = ListUtils.newArrayList();
+		List<Long> trendOutput = ListUtils.newArrayList();
+		List<Long> trendPlanQty = ListUtils.newArrayList();
+		DateTimeFormatter trendFmt = DateTimeFormatter.ofPattern("M/d");
+		for (int i = 6; i >= 0; i--) {
+			LocalDate trendDay = day.minusDays(i);
+			String trendDayStr = trendDay.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+			String trendStart = trendDayStr + " 08:00:00";
+			String trendEnd = trendDay.plusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + " 08:00:00";
+			long trendPlan = i == 0 ? plan : sumPlanNums(loadPlans(trendDayStr), null);
+			long trendActual = i == 0 ? actual : countFinished(trendStart, trendEnd, null);
+			long trendOk = i == 0 ? ok : countCraftResult("OK", trendStart, trendEnd);
+			long trendNg = i == 0 ? ng : countCraftResult("NG", trendStart, trendEnd);
+			long trendRepair = i == 0 ? repair : countRepair(trendStart, trendEnd);
+			double trendAchieveRate = ratio(trendActual, trendPlan, false);
+			double trendFpyRate = firstPassRate(trendOk, trendNg, trendActual, trendRepair);
+			double trendPerformance = trendPlan <= 0 ? 0D : ratio(trendActual, trendPlan, true);
+			double trendAvailability = i == 0 ? availability : 100D;
+			trendLabels.add(trendDay.format(trendFmt));
+			trendAchieve.add(trendAchieveRate);
+			trendFpy.add(trendFpyRate);
+			trendOee.add(round1(trendAvailability * trendPerformance * trendFpyRate / 10000D));
+			trendOutput.add(trendActual);
+			trendPlanQty.add(trendPlan);
+		}
+
+		Map<String, Object> summary = MapUtils.newHashMap();
+		summary.put("plan", plan);
+		summary.put("actual", actual);
+		summary.put("ok", ok);
+		summary.put("ng", ng);
+		summary.put("repair", repair);
+		summary.put("openAlarm", openAlarm);
+
+		Map<String, Object> kpi = MapUtils.newHashMap();
+		kpi.put("achieve", achieve);
+		kpi.put("fpy", fpy);
+		kpi.put("oee", oee);
+		kpi.put("availability", availability);
+		kpi.put("performance", performance);
+		kpi.put("quality", fpy);
+		kpi.put("shiftAchieve", curShiftAchieve);
+
+		Map<String, Object> hours = MapUtils.newHashMap();
+		hours.put("labels", hourLabels);
+		hours.put("actual", hourActual);
+		hours.put("plan", hourPlan);
+
+		Map<String, Object> trend = MapUtils.newHashMap();
+		trend.put("labels", trendLabels);
+		trend.put("achieve", trendAchieve);
+		trend.put("fpy", trendFpy);
+		trend.put("oee", trendOee);
+		trend.put("output", trendOutput);
+		trend.put("planQty", trendPlanQty);
+
+		Map<String, Object> data = MapUtils.newHashMap();
+		data.put("day", dayStr);
+		data.put("window", "08:00-次日08:00");
+		data.put("shiftTitle", curShiftTitle);
+		data.put("summary", summary);
+		data.put("kpi", kpi);
+		data.put("hours", hours);
+		data.put("trend", trend);
+		data.put("shifts", shiftRows);
+		data.put("products", productRows);
+		resp.setData(data);
+		resp.setResult(Global.TRUE);
+		return resp;
+	}
+
+	private List<MesPlan> loadPlans(String day) {
+		MesPlan query = new MesPlan();
+		query.setStatus("0");
+		query.setDay(day);
+		List<MesPlan> plans = mesPlanService.findList(query);
+		return plans == null ? ListUtils.newArrayList() : plans;
+	}
+
+	private long sumPlanNums(List<MesPlan> plans, String shiftId) {
+		long sum = 0L;
+		for (MesPlan plan : plans) {
+			if (plan.getNums() == null) {
+				continue;
+			}
+			if (StringUtils.isNotEmpty(shiftId) && !StringUtils.equals(shiftId, plan.getShiftId())) {
+				continue;
+			}
+			sum += plan.getNums();
+		}
+		return sum;
+	}
+
+	private List<Map<String, Object>> buildProductRows(List<MesPlan> plans, String start, String end) {
+		Map<String, Map<String, Object>> grouped = new LinkedHashMap<>();
+		for (MesPlan plan : plans) {
+			String key = StringUtils.isNotEmpty(plan.getCateSn()) ? plan.getCateSn() : plan.getCateId();
+			if (StringUtils.isEmpty(key)) {
+				continue;
+			}
+			Map<String, Object> row = grouped.get(key);
+			if (row == null) {
+				row = MapUtils.newHashMap();
+				row.put("title", StringUtils.isNotEmpty(plan.getCateTitle()) ? plan.getCateTitle() : key);
+				row.put("sn", key);
+				row.put("plan", 0L);
+				grouped.put(key, row);
+			}
+			long planNums = row.get("plan") instanceof Number ? ((Number) row.get("plan")).longValue() : 0L;
+			row.put("plan", planNums + (plan.getNums() == null ? 0 : plan.getNums()));
+		}
+		List<Map<String, Object>> rows = ListUtils.newArrayList();
+		for (Map.Entry<String, Map<String, Object>> entry : grouped.entrySet()) {
+			Map<String, Object> row = entry.getValue();
+			long plan = ((Number) row.get("plan")).longValue();
+			long actual = countFinished(start, end, entry.getKey());
+			row.put("actual", actual);
+			row.put("achieve", ratio(actual, plan, false));
+			rows.add(row);
+			if (rows.size() >= 8) {
+				break;
+			}
+		}
+		return rows;
+	}
+
+	private long countFinished(String start, String end, String cate) {
+		MesProduct query = new MesProduct();
+		query.setState("1");
+		if (StringUtils.isNotEmpty(cate)) {
+			query.setCate(cate);
+		}
+		query.getSqlMap().getWhere()
+				.and("update_date", QueryType.GTE, start)
+				.and("update_date", QueryType.LT, end);
+		Long count = mesProductService.findCount(query);
+		return count == null ? 0L : count;
+	}
+
+	private long countCraftResult(String content, String start, String end) {
+		MesProductRecord query = new MesProductRecord();
+		query.setCraft("100000");
+		query.setContent(content);
+		query.getSqlMap().getWhere()
+				.and("update_date", QueryType.GTE, start)
+				.and("update_date", QueryType.LT, end);
+		Long count = mesProductRecordService.findCount(query);
+		return count == null ? 0L : count;
+	}
+
+	private long countRepair(String start, String end) {
+		MesRepair query = new MesRepair();
+		query.getSqlMap().getWhere()
+				.and("a.create_date", QueryType.GTE, start)
+				.and("a.create_date", QueryType.LT, end);
+		Long count = mesRepairService.findCount(query);
+		return count == null ? 0L : count;
+	}
+
+	private long countOpenAlarm() {
+		MesProcessAlarm query = new MesProcessAlarm();
+		query.setState("0");
+		Long count = mesProcessAlarmService.findCount(query);
+		return count == null ? 0L : count;
+	}
+
+	private long countStations() {
+		MesLineProcess query = new MesLineProcess();
+		query.setStatus("0");
+		query.getSqlMap().getWhere().and("pid", QueryType.IS_NULL, "");
+		Long count = mesLineProcessService.findCount(query);
+		return count == null ? 0L : count;
+	}
+
+	private String[] shiftWindow(LocalDate day, MesShift shift) {
+		String startHm = hourMinute(shift.getStart());
+		String endHm = hourMinute(shift.getEnd());
+		if (StringUtils.isEmpty(startHm) || StringUtils.isEmpty(endHm)) {
+			return null;
+		}
+		LocalDate endDay = shift.getNext() != null && shift.getNext() == 1 ? day.plusDays(1) : day;
+		String start = day.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + " " + startHm + ":00";
+		String end = endDay.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + " " + endHm + ":00";
+		return new String[]{start, end, startHm + "-" + endHm};
+	}
+
+	private String hourMinute(String time) {
+		if (StringUtils.isEmpty(time)) {
+			return "";
+		}
+		String value = time.trim();
+		return value.length() >= 5 ? value.substring(0, 5) : value;
+	}
+
+	private double firstPassRate(long ok, long ng, long actual, long repair) {
+		if (ok + ng > 0) {
+			return ratio(ok, ok + ng, true);
+		}
+		if (actual <= 0) {
+			return 0D;
+		}
+		return ratio(Math.max(0L, actual - repair), actual, true);
+	}
+
+	private double availabilityRate(long openAlarm, long stations) {
+		if (stations <= 0 || openAlarm <= 0) {
+			return 100D;
+		}
+		double down = Math.min(100D, openAlarm * 100D / stations);
+		return round1(Math.max(0D, 100D - down));
+	}
+
+	private double ratio(long numerator, long denominator, boolean capAt100) {
+		if (denominator <= 0) {
+			return 0D;
+		}
+		double value = numerator * 100D / denominator;
+		if (value < 0) {
+			value = 0D;
+		}
+		if (capAt100 && value > 100D) {
+			value = 100D;
+		}
+		return round1(value);
+	}
+
+	private double round1(double value) {
+		if (Double.isNaN(value) || Double.isInfinite(value)) {
+			return 0D;
+		}
+		return Math.round(value * 10D) / 10D;
+	}
 }

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

@@ -144,6 +144,9 @@ public class MesProductRecordController extends BaseController {
 	private MesLineService mesLineService;
 
 	@Autowired
+	private MesWorkOrderService mesWorkOrderService;
+
+	@Autowired
 	private MesLineProcessService mesLineProcessService;
 
 	@Autowired
@@ -757,6 +760,14 @@ public class MesProductRecordController extends BaseController {
 			}
 		}
 
+		MesWorkOrderService.OnlineMatch match = mesWorkOrderService.matchOnline(lineSn, sn, cateSn);
+		if(!match.isOk()){
+			return "RS" + match.getCode();
+		}
+		if(match.getOrder() != null){
+			cateSn = match.getOrder().getCateSn();
+		}
+
 		MesProductRecord mpr = mesProductRecordService.findBySnCate(sn,cateSn);
 		if(!ObjectUtils.isEmpty(mpr)){
 			return "RSCF";  // 工件码重复
@@ -769,6 +780,9 @@ public class MesProductRecordController extends BaseController {
 		mesProductRecord.setContent(retokng);
 		mesProductRecord.setCreateBy(user);
 		mesProductRecord.setCateSn(cateSn);
+		if(match.getOrder() != null){
+			mesProductRecord.setWorkNo(match.getOrder().getWorkNo());
+		}
 
 		// 质量上传,解析参数
 		List<ParamsResp> params = mesProductRecordService.getQualityParams(oprno,msg);
@@ -859,6 +873,14 @@ public class MesProductRecordController extends BaseController {
 			}
 		}
 
+		MesWorkOrderService.OnlineMatch match = mesWorkOrderService.matchOnline(lineSn, sn, cateSn);
+		if(!match.isOk()){
+			return "RS" + match.getCode();
+		}
+		if(match.getOrder() != null){
+			cateSn = match.getOrder().getCateSn();
+		}
+
 		MesProductRecord mpr = mesProductRecordService.findBySnCate(sn,cateSn);
 		if(!ObjectUtils.isEmpty(mpr)){
 			return "RSCF";  // 工件码重复
@@ -871,6 +893,9 @@ public class MesProductRecordController extends BaseController {
 		mesProductRecord.setContent(retokng);
 		mesProductRecord.setCreateBy(user);
 		mesProductRecord.setCateSn(cateSn);
+		if(match.getOrder() != null){
+			mesProductRecord.setWorkNo(match.getOrder().getWorkNo());
+		}
 
 		// 质量上传,解析参数
 		List<ParamsResp> params = new ArrayList<ParamsResp>();

+ 77 - 0
src/main/java/com/jeesite/modules/mes/web/MesScreenController.java

@@ -0,0 +1,77 @@
+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.MesScreen;
+import com.jeesite.modules.mes.service.MesScreenService;
+
+/**
+ * 大屏管理
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/mesScreen")
+public class MesScreenController extends BaseController {
+
+	@Autowired
+	private MesScreenService mesScreenService;
+
+	@ModelAttribute
+	public MesScreen get(String id, boolean isNewRecord) {
+		return mesScreenService.get(id, isNewRecord);
+	}
+
+	@RequiresPermissions("mes:mesScreen:view")
+	@RequestMapping(value = {"list", ""})
+	public String list(MesScreen mesScreen, Model model) {
+		model.addAttribute("mesScreen", mesScreen);
+		return "modules/mes/mesScreenList";
+	}
+
+	@RequiresPermissions("mes:mesScreen:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MesScreen> listData(MesScreen mesScreen, HttpServletRequest request, HttpServletResponse response) {
+		mesScreen.setPage(new Page<>(request, response));
+		return mesScreenService.findPage(mesScreen);
+	}
+
+	@RequiresPermissions("mes:mesScreen:view")
+	@RequestMapping(value = "form")
+	public String form(MesScreen mesScreen, Model model) {
+		if (mesScreen.getIsNewRecord() && mesScreen.getSortNo() == null) {
+			mesScreen.setSortNo(30);
+		}
+		model.addAttribute("mesScreen", mesScreen);
+		return "modules/mes/mesScreenForm";
+	}
+
+	@RequiresPermissions("mes:mesScreen:edit")
+	@PostMapping(value = "save")
+	@ResponseBody
+	public String save(@Validated MesScreen mesScreen) {
+		mesScreenService.save(mesScreen);
+		return renderResult(Global.TRUE, text("保存大屏成功!"));
+	}
+
+	@RequiresPermissions("mes:mesScreen:edit")
+	@RequestMapping(value = "delete")
+	@ResponseBody
+	public String delete(MesScreen mesScreen) {
+		mesScreenService.delete(mesScreen);
+		return renderResult(Global.TRUE, text("删除大屏成功!"));
+	}
+}

+ 188 - 0
src/main/java/com/jeesite/modules/mes/web/MesWorkOrderController.java

@@ -0,0 +1,188 @@
+package com.jeesite.modules.mes.web;
+
+import com.jeesite.common.collect.ListUtils;
+import com.jeesite.common.config.Global;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.DateUtils;
+import com.jeesite.common.utils.excel.ExcelExport;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Type;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.modules.mes.entity.MesLine;
+import com.jeesite.modules.mes.entity.MesProductCate;
+import com.jeesite.modules.mes.entity.MesWorkOrder;
+import com.jeesite.modules.mes.entity.MesWorkOrderSn;
+import com.jeesite.modules.mes.service.MesLineService;
+import com.jeesite.modules.mes.service.MesProductCateService;
+import com.jeesite.modules.mes.service.MesWorkOrderService;
+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 org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 工单Controller。只维护单据,不改现场上线。
+ * @author mes
+ * @version 2026-10-10
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/mesWorkOrder")
+public class MesWorkOrderController extends BaseController {
+
+	@Autowired
+	private MesWorkOrderService mesWorkOrderService;
+
+	@Autowired
+	private MesLineService mesLineService;
+
+	@Autowired
+	private MesProductCateService mesProductCateService;
+
+	@ModelAttribute
+	public MesWorkOrder get(String id, boolean isNewRecord) {
+		return mesWorkOrderService.get(id, isNewRecord);
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = {"list", ""})
+	public String list(MesWorkOrder mesWorkOrder, Model model) {
+		model.addAttribute("mesWorkOrder", mesWorkOrder);
+		model.addAttribute("lines", distinctLines());
+		model.addAttribute("cates", productCates());
+		return "modules/mes/mesWorkOrderList";
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MesWorkOrder> listData(MesWorkOrder mesWorkOrder, HttpServletRequest request, HttpServletResponse response) {
+		mesWorkOrder.setPage(new Page<>(request, response));
+		return mesWorkOrderService.findPage(mesWorkOrder);
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = "snList")
+	public String snList(MesWorkOrderSn mesWorkOrderSn, Model model) {
+		model.addAttribute("mesWorkOrderSn", mesWorkOrderSn);
+		return "modules/mes/mesWorkOrderSnList";
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = "snListData")
+	@ResponseBody
+	public Page<MesWorkOrderSn> snListData(MesWorkOrderSn mesWorkOrderSn, HttpServletRequest request, HttpServletResponse response) {
+		mesWorkOrderSn.setPage(new Page<>(request, response));
+		return mesWorkOrderService.findSnPage(mesWorkOrderSn);
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = "form")
+	public String form(MesWorkOrder mesWorkOrder, Model model) {
+		model.addAttribute("mesWorkOrder", mesWorkOrder);
+		model.addAttribute("lines", distinctLines());
+		model.addAttribute("cates", productCates());
+		return "modules/mes/mesWorkOrderForm";
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:edit")
+	@PostMapping(value = "save")
+	@ResponseBody
+	public String save(@Validated MesWorkOrder mesWorkOrder) {
+		mesWorkOrderService.save(mesWorkOrder);
+		return renderResult(Global.TRUE, text("保存工单成功!"));
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:edit")
+	@RequestMapping(value = "delete")
+	@ResponseBody
+	public String delete(MesWorkOrder mesWorkOrder) {
+		mesWorkOrderService.delete(mesWorkOrder);
+		return renderResult(Global.TRUE, text("删除工单成功!"));
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:edit")
+	@RequestMapping(value = "issue")
+	@ResponseBody
+	public String issue(MesWorkOrder mesWorkOrder) {
+		mesWorkOrderService.issue(mesWorkOrder);
+		return renderResult(Global.TRUE, text("下达工单成功!"));
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:edit")
+	@RequestMapping(value = "recall")
+	@ResponseBody
+	public String recall(MesWorkOrder mesWorkOrder) {
+		mesWorkOrderService.recall(mesWorkOrder);
+		return renderResult(Global.TRUE, text("撤回工单成功!"));
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:edit")
+	@RequestMapping(value = "close")
+	@ResponseBody
+	public String close(MesWorkOrder mesWorkOrder) {
+		mesWorkOrderService.close(mesWorkOrder);
+		return renderResult(Global.TRUE, text("关闭工单成功!"));
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = "exportData")
+	public void exportData(MesWorkOrder mesWorkOrder, HttpServletResponse response) {
+		List<MesWorkOrder> list = mesWorkOrderService.findList(mesWorkOrder);
+		String fileName = "工单" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+		try (ExcelExport ee = new ExcelExport("工单排程", MesWorkOrder.class)) {
+			ee.setDataList(list).write(response, fileName);
+		}
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = "importTemplate")
+	public void importTemplate(HttpServletResponse response) {
+		MesWorkOrder sample = new MesWorkOrder();
+		List<MesWorkOrder> list = ListUtils.newArrayList(sample);
+		String fileName = "武汉厂工单排程表模板.xlsx";
+		try (ExcelExport ee = new ExcelExport("工单排程", MesWorkOrder.class, Type.IMPORT)) {
+			ee.setDataList(list).write(response, fileName);
+		}
+	}
+
+	private List<MesLine> distinctLines() {
+		Map<String, MesLine> lines = new LinkedHashMap<String, MesLine>();
+		for (MesLine line : mesLineService.findList(new MesLine())) {
+			if (line != null && StringUtils.isNotBlank(line.getSn()) && !lines.containsKey(line.getSn())) {
+				lines.put(line.getSn(), line);
+			}
+		}
+		return new ArrayList<MesLine>(lines.values());
+	}
+
+	private List<MesProductCate> productCates() {
+		MesProductCate query = new MesProductCate();
+		query.setCate(0);
+		return mesProductCateService.findList(query);
+	}
+
+	@ResponseBody
+	@RequiresPermissions("mes:mesWorkOrder:edit")
+	@PostMapping(value = "importData")
+	public String importData(MultipartFile file) {
+		try {
+			String message = mesWorkOrderService.importData(file);
+			return renderResult(Global.TRUE, "posfull:" + message);
+		} catch (Exception ex) {
+			return renderResult(Global.FALSE, "posfull:" + ex.getMessage());
+		}
+	}
+}

+ 8 - 0
src/main/java/com/jeesite/modules/utils/ErrorMsg.java

@@ -72,6 +72,14 @@ public class ErrorMsg {
                 lmsg = "上一工位完成后冷却时间未到";
             } else if (processMsgRet.equalsIgnoreCase("RP")) {
                 lmsg = "该精追码已在本工位加工完成,不可重复";
+            } else if (processMsgRet.equalsIgnoreCase("WO")) {
+                lmsg = "当前产线没有已下达或生产中的工单";
+            } else if (processMsgRet.equalsIgnoreCase("WT")) {
+                lmsg = "工件类型与当前工单不一致";
+            } else if (processMsgRet.equalsIgnoreCase("WQ")) {
+                lmsg = "工单计划数量已完成";
+            } else if (processMsgRet.equalsIgnoreCase("WC")) {
+                lmsg = "工单未选择工件类型";
             }
         }catch (Exception e){ }
         return lmsg;

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

@@ -628,6 +628,9 @@ shiro:
     ${adminPath}/mes/mesProduct/screen7 = anon
     ${adminPath}/mes/mesProduct/screen8 = anon
     ${adminPath}/mes/mesProduct/screen9 = anon
+    ${adminPath}/mes/mesProduct/screen10 = anon
+    ${adminPath}/mes/mesProduct/screen11 = anon
+    ${adminPath}/mes/mesProduct/screenKpiData = anon
     ${adminPath}/mes/mesProduct/screenRepairData = anon
     ${adminPath}/mes/mesProduct/screenData = anon
     ${adminPath}/mes/mesProduct/screenData4 = anon

+ 5 - 0
src/main/resources/mappings/modules/mes/MesScreenDao.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.MesScreenDao">
+
+</mapper>

+ 141 - 0
src/main/resources/mappings/modules/mes/MesWorkOrderDao.xml

@@ -0,0 +1,141 @@
+<?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.MesWorkOrderDao">
+
+	<select id="findByWorkNo" resultType="com.jeesite.modules.mes.entity.MesWorkOrder">
+		SELECT
+			a.id AS "id",
+			a.work_no AS "workNo",
+			a.project_code AS "projectCode",
+			a.material_sn AS "materialSn",
+			a.spec_name AS "specName",
+			a.drawing_no AS "drawingNo",
+			a.plan_qty AS "planQty",
+			a.open_date AS "openDate",
+			a.produce_date AS "produceDate",
+			a.sale_order_no AS "saleOrderNo",
+			a.line_sn AS "lineSn",
+			a.cate_sn AS "cateSn",
+			a.state AS "state",
+			a.done_qty AS "doneQty",
+			a.status AS "status",
+			a.remarks AS "remarks"
+		FROM mes_work_order a
+		WHERE a.work_no = #{workNo}
+		LIMIT 1
+	</select>
+
+	<select id="findOnline" resultType="com.jeesite.modules.mes.entity.MesWorkOrder">
+		SELECT
+			a.id AS "id",
+			a.work_no AS "workNo",
+			a.material_sn AS "materialSn",
+			a.plan_qty AS "planQty",
+			a.line_sn AS "lineSn",
+			a.cate_sn AS "cateSn",
+			a.state AS "state",
+			a.done_qty AS "doneQty",
+			a.status AS "status"
+		FROM mes_work_order a
+		WHERE a.line_sn = #{lineSn}
+			AND a.status = '0'
+			AND a.state IN ('1', '2')
+		ORDER BY a.state DESC, a.produce_date ASC, a.create_date ASC
+	</select>
+
+	<select id="countOnlineOk" resultType="int">
+		SELECT COUNT(1)
+		FROM mes_product_record
+		WHERE work_no = #{workNo}
+			AND content = 'OK'
+	</select>
+
+	<select id="findSnList" resultType="com.jeesite.modules.mes.entity.MesWorkOrderSn">
+		SELECT
+			x.id AS "id",
+			x.workNo AS "workNo",
+			x.sn AS "sn",
+			x.cateSn AS "cateSn",
+			x.cateTitle AS "cateTitle",
+			x.lineSn AS "lineSn",
+			x.lineTitle AS "lineTitle",
+			x.onlineDate AS "onlineDate",
+			x.productState AS "productState",
+			x.currentOprno AS "currentOprno",
+			x.stationResult AS "stationResult",
+			(
+				SELECT lp.title
+				FROM mes_line_process lp
+				INNER JOIN mes_line l ON l.id = lp.line_id AND l.status = '0'
+				WHERE lp.status = '0'
+					AND lp.oprno = x.currentOprno
+					AND l.sn = x.lineSn
+				ORDER BY lp.id
+				LIMIT 1
+			) AS "oprnoTitle"
+		FROM (
+			SELECT
+				r.id AS id,
+				r.work_no AS workNo,
+				r.sn AS sn,
+				r.cate_sn AS cateSn,
+				c.title AS cateTitle,
+				r.line_sn AS lineSn,
+				ln.title AS lineTitle,
+				r.create_date AS onlineDate,
+				(
+					SELECT p.state
+					FROM mes_product p
+					WHERE p.sn = r.sn
+					ORDER BY p.update_date DESC
+					LIMIT 1
+				) AS productState,
+				(
+					SELECT t.oprno
+					FROM mes_product_record t
+					WHERE t.sn = r.sn
+						AND t.craft = '100000'
+						AND t.content IN ('OK', 'NG')
+					ORDER BY t.update_date DESC, t.id DESC
+					LIMIT 1
+				) AS currentOprno,
+				(
+					SELECT t.content
+					FROM mes_product_record t
+					WHERE t.sn = r.sn
+						AND t.craft = '100000'
+						AND t.content IN ('OK', 'NG')
+					ORDER BY t.update_date DESC, t.id DESC
+					LIMIT 1
+				) AS stationResult
+			FROM mes_product_record r
+			LEFT JOIN mes_product_cate c ON c.sn = r.cate_sn AND c.status = '0'
+			LEFT JOIN mes_line ln ON ln.sn = r.line_sn AND ln.status = '0'
+				AND ln.id = (
+					SELECT MIN(l2.id) FROM mes_line l2 WHERE l2.sn = r.line_sn AND l2.status = '0'
+				)
+			WHERE r.work_no IS NOT NULL AND r.work_no != ''
+			<if test="workNo != null and workNo != ''">
+				AND r.work_no LIKE CONCAT('%', #{workNo}, '%')
+			</if>
+			<if test="sn != null and sn != ''">
+				AND r.sn LIKE CONCAT('%', #{sn}, '%')
+			</if>
+		) x
+		<where>
+			<if test="productState != null and productState != ''">
+				AND x.productState = #{productState}
+			</if>
+		</where>
+		ORDER BY x.onlineDate DESC, x.sn ASC
+	</select>
+
+	<update id="updateDone">
+		UPDATE mes_work_order
+		SET done_qty = #{doneQty},
+			state = #{state},
+			update_date = NOW()
+		WHERE id = #{id}
+	</update>
+
+</mapper>

+ 274 - 0
src/main/resources/static/screen/exec-screen.css

@@ -0,0 +1,274 @@
+html,
+body {
+	width: 100%;
+	height: 100%;
+	margin: 0;
+	overflow: hidden;
+	background: #07111f;
+	color: #e7f3ff;
+	font-family: "Microsoft YaHei", Arial, sans-serif;
+}
+
+.exec-page {
+	height: 100%;
+	min-width: 1180px;
+	box-sizing: border-box;
+	padding: 16px 20px 14px;
+	display: flex;
+	flex-direction: column;
+	gap: 12px;
+	background:
+		radial-gradient(1000px 360px at 50% -12%, rgba(28, 96, 170, 0.32), transparent 62%),
+		linear-gradient(180deg, #0c1d34 0%, #07111f 100%);
+}
+
+.exec-header {
+	height: 46px;
+	flex-shrink: 0;
+	display: grid;
+	grid-template-columns: 1fr auto 1fr;
+	align-items: center;
+}
+
+.exec-header h1 {
+	margin: 0;
+	font-size: 28px;
+	letter-spacing: 6px;
+	text-align: center;
+}
+
+.header-side {
+	display: flex;
+	align-items: center;
+	gap: 8px;
+}
+
+.header-side.is-right {
+	justify-content: flex-end;
+}
+
+.meta-chip,
+.screen-time {
+	color: #9ec9ee;
+	font-size: 14px;
+}
+
+.meta-chip {
+	height: 28px;
+	padding: 0 12px;
+	border: 1px solid rgba(90, 168, 230, 0.32);
+	border-radius: 14px;
+	line-height: 28px;
+	background: rgba(8, 28, 54, 0.7);
+}
+
+.hero-grid,
+.main-grid,
+.bottom-grid {
+	min-height: 0;
+	display: grid;
+	gap: 12px;
+}
+
+.hero-grid {
+	flex-shrink: 0;
+	grid-template-columns: 1.15fr 1fr 1fr 1fr;
+}
+
+.main-grid {
+	flex: 1.25;
+	grid-template-columns: 1.7fr 0.9fr;
+}
+
+.bottom-grid {
+	flex: 0.85;
+	grid-template-columns: 1.15fr 0.85fr;
+}
+
+.hero-card,
+.panel {
+	border: 1px solid rgba(86, 156, 214, 0.28);
+	border-radius: 10px;
+	background: rgba(8, 28, 52, 0.82);
+	box-sizing: border-box;
+}
+
+.hero-card {
+	padding: 14px 18px 16px;
+}
+
+.hero-label,
+.panel-title {
+	color: #9ec9ee;
+	font-size: 14px;
+}
+
+.hero-num {
+	margin-top: 6px;
+	font-size: 40px;
+	font-weight: 700;
+	line-height: 1.05;
+	font-family: Arial, "Microsoft YaHei", sans-serif;
+}
+
+.hero-sub,
+.hero-foot {
+	margin-top: 8px;
+	color: #d5e8f8;
+	font-size: 14px;
+}
+
+.hero-foot {
+	display: flex;
+	align-items: center;
+	gap: 8px;
+}
+
+.meter {
+	height: 8px;
+	margin-top: 12px;
+	border-radius: 4px;
+	background: rgba(255, 255, 255, 0.08);
+	overflow: hidden;
+}
+
+.meter i,
+.track i {
+	display: block;
+	height: 100%;
+	width: 0;
+	border-radius: 4px;
+	background: #3ec6ff;
+}
+
+.tag {
+	height: 22px;
+	padding: 0 8px;
+	border-radius: 11px;
+	font-style: normal;
+	font-size: 12px;
+	line-height: 22px;
+	color: #d7ecff;
+	background: rgba(255, 255, 255, 0.08);
+}
+
+.tag.is-good,
+.track i.is-good { background: #5ddea0; }
+.tag.is-good { color: #14301f; }
+.tag.is-ok,
+.track i.is-ok,
+.meter i.is-ok { background: #7fd4ff; }
+.tag.is-ok { color: #062033; }
+.tag.is-warn,
+.track i.is-warn,
+.meter i.is-warn { background: #ffd36a; }
+.tag.is-warn { color: #3a2a04; }
+.tag.is-bad,
+.track i.is-bad,
+.meter i.is-bad { background: #ef6b6b; }
+.tag.is-bad { color: #fff; }
+.meter i.is-good { background: #5ddea0; }
+
+.panel {
+	min-width: 0;
+	min-height: 0;
+	display: flex;
+	flex-direction: column;
+	padding: 12px 14px 10px;
+}
+
+.panel-title {
+	height: 24px;
+	flex-shrink: 0;
+	color: #d7ecff;
+	font-size: 15px;
+	font-weight: 700;
+}
+
+.chart-box,
+.bar-list,
+.attention-list {
+	flex: 1;
+	min-height: 0;
+}
+
+.chart-panel {
+	position: relative;
+}
+
+.chart-panel .chart-box {
+	position: absolute;
+	left: 14px;
+	right: 14px;
+	top: 36px;
+	bottom: 8px;
+}
+
+.attention-list,
+.bar-list {
+	margin: 8px 0 0;
+	padding: 0;
+	overflow: auto;
+	list-style: none;
+}
+
+.attention-list li {
+	margin: 0 0 12px;
+	padding-left: 14px;
+	position: relative;
+	color: #e7f3ff;
+	font-size: 15px;
+	line-height: 1.5;
+}
+
+.attention-list li:before {
+	content: "";
+	position: absolute;
+	left: 0;
+	top: 0.55em;
+	width: 6px;
+	height: 6px;
+	border-radius: 50%;
+	background: #ffd36a;
+}
+
+.attention-list li.is-ok:before {
+	background: #5ddea0;
+}
+
+.bar-row {
+	display: grid;
+	grid-template-columns: 120px 1fr 150px;
+	gap: 10px;
+	align-items: center;
+	margin-bottom: 12px;
+}
+
+.bar-name,
+.bar-meta {
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	font-size: 14px;
+}
+
+.bar-name {
+	color: #fff;
+}
+
+.bar-meta {
+	color: #b7d4ec;
+	text-align: right;
+}
+
+.track {
+	height: 10px;
+	border-radius: 5px;
+	background: rgba(255, 255, 255, 0.08);
+	overflow: hidden;
+}
+
+.empty-tip {
+	color: #8eb0c8;
+	font-size: 14px;
+}

+ 316 - 0
src/main/resources/static/screen/exec-screen.js

@@ -0,0 +1,316 @@
+(function ($, window) {
+	"use strict";
+
+	var config = window.execScreenConfig || {};
+	var chart = null;
+	var refreshTimer = null;
+
+	function pad(num) {
+		return num < 10 ? "0" + num : String(num);
+	}
+
+	function updateClock() {
+		var now = new Date();
+		$("#screenTime").text(
+			now.getFullYear() + "-" + pad(now.getMonth() + 1) + "-" + pad(now.getDate()) +
+			" " + pad(now.getHours()) + ":" + pad(now.getMinutes()) + ":" + pad(now.getSeconds())
+		);
+	}
+
+	function escapeHtml(text) {
+		return String(text == null ? "" : text)
+			.replace(/&/g, "&amp;")
+			.replace(/</g, "&lt;")
+			.replace(/>/g, "&gt;")
+			.replace(/"/g, "&quot;");
+	}
+
+	function num(value) {
+		var n = Number(value);
+		return isNaN(n) ? 0 : n;
+	}
+
+	function rateText(value) {
+		return num(value).toFixed(1) + "%";
+	}
+
+	function level(value) {
+		var n = num(value);
+		if (n >= 95) {
+			return "is-good";
+		}
+		if (n >= 85) {
+			return "is-ok";
+		}
+		if (n >= 70) {
+			return "is-warn";
+		}
+		return "is-bad";
+	}
+
+	function levelName(value) {
+		var name = level(value);
+		if (name === "is-good") {
+			return "正常";
+		}
+		if (name === "is-ok") {
+			return "基本正常";
+		}
+		if (name === "is-warn") {
+			return "偏慢";
+		}
+		return "明显落后";
+	}
+
+	function slowest(rows) {
+		var picked = null;
+		(rows || []).forEach(function (row) {
+			if (num(row.plan) <= 0 || num(row.achieve) >= 85) {
+				return;
+			}
+			if (!picked || num(row.achieve) < num(picked.achieve)) {
+				picked = row;
+			}
+		});
+		return picked;
+	}
+
+	function sum(list) {
+		var total = 0;
+		(list || []).forEach(function (item) {
+			total += num(item);
+		});
+		return total;
+	}
+
+	function average(list) {
+		if (!list || !list.length) {
+			return 0;
+		}
+		return sum(list) / list.length;
+	}
+
+	function renderHero(data) {
+		var summary = data.summary || {};
+		var kpi = data.kpi || {};
+		var trend = data.trend || {};
+		var plan = num(summary.plan);
+		var actual = num(summary.actual);
+		var gap = plan - actual;
+		var weekOut = sum(trend.output);
+		var weekPlan = sum(trend.planQty);
+		var weekAvg = average((trend.achieve || []).slice(0, -1));
+
+		$("#statDay").text("统计日 " + (data.day || "--"));
+		$("#shiftChip").text("当前班次 " + (data.shiftTitle || "未排班"));
+		$("#actualNum").text(actual);
+		$("#planText").text(plan > 0 ? ("计划 " + plan) : "今日未排计划");
+
+		if (plan <= 0) {
+			$("#gapLabel").text("计划差额");
+			$("#gapNum").text("--");
+			$("#achieveText").text("未排计划");
+			$("#achieveTag").removeClass("is-good is-ok is-warn is-bad").text("待排产");
+			$("#achieveBar").css("width", "0");
+		} else if (gap > 0) {
+			$("#gapLabel").text("还差");
+			$("#gapNum").text(gap);
+			$("#achieveText").text("达成 " + rateText(kpi.achieve));
+			paintAchieve(kpi.achieve);
+		} else if (gap < 0) {
+			$("#gapLabel").text("超出计划");
+			$("#gapNum").text(Math.abs(gap));
+			$("#achieveText").text("达成 " + rateText(kpi.achieve));
+			paintAchieve(kpi.achieve);
+		} else {
+			$("#gapLabel").text("差额");
+			$("#gapNum").text("0");
+			$("#achieveText").text("已完成计划");
+			paintAchieve(kpi.achieve);
+		}
+
+		var inspected = num(summary.ok) + num(summary.ng);
+		$("#fpyNum").text(inspected > 0 || actual > 0 ? rateText(kpi.fpy) : "--");
+		$("#qualityText").text("返修 " + num(summary.repair) + " · 不良 " + num(summary.ng) + " · 未关报警 " + num(summary.openAlarm));
+		$("#weekActual").text(weekOut);
+		$("#weekText").text("计划 " + weekPlan + (weekAvg ? (" · 前几日平均达成 " + weekAvg.toFixed(1) + "%") : ""));
+	}
+
+	function paintAchieve(value) {
+		var cls = level(value);
+		$("#achieveTag").removeClass("is-good is-ok is-warn is-bad").addClass(cls).text(levelName(value));
+		$("#achieveBar").css("width", Math.max(0, Math.min(num(value), 100)) + "%")
+			.removeClass("is-good is-ok is-warn is-bad").addClass(cls);
+	}
+
+	function renderAttention(data) {
+		var summary = data.summary || {};
+		var kpi = data.kpi || {};
+		var trend = data.trend || {};
+		var plan = num(summary.plan);
+		var items = [];
+
+		if (plan <= 0) {
+			items.push({ text: "今天还没有班次生产计划,完成情况无法和目标对比。", ok: false });
+		}
+
+		var history = (trend.achieve || []).slice(0, -1);
+		if (plan > 0 && history.length) {
+			var avg = average(history);
+			var diff = num(kpi.achieve) - avg;
+			if (diff <= -8) {
+				items.push({ text: "今天比前几日平均达成低 " + Math.abs(diff).toFixed(1) + " 个百分点,不是平时的水平。", ok: false });
+			}
+		}
+
+		if (num(kpi.fpy) > 0 && num(kpi.fpy) < 95) {
+			items.push({ text: "一次合格率 " + rateText(kpi.fpy) + ",低于 95%。", ok: false });
+		}
+		if (num(summary.actual) > 0 && num(summary.repair) * 100 / num(summary.actual) >= 2) {
+			items.push({ text: "返修 " + num(summary.repair) + " 件,约占完工的 " + (num(summary.repair) * 100 / num(summary.actual)).toFixed(1) + "%。", ok: false });
+		}
+		if (num(summary.openAlarm) > 0) {
+			items.push({ text: "还有 " + num(summary.openAlarm) + " 条报警没有关闭。", ok: false });
+		}
+
+		var slowProduct = slowest(data.products);
+		if (slowProduct) {
+			items.push({ text: slowProduct.title + "是最慢的产品,完成 " + num(slowProduct.actual) + " / " + num(slowProduct.plan) + "。", ok: false });
+		}
+		var slowShift = slowest(data.shifts);
+		if (slowShift) {
+			items.push({ text: slowShift.title + "是最慢的班次,完成 " + num(slowShift.actual) + " / " + num(slowShift.plan) + "。", ok: false });
+		}
+
+		if (!items.length) {
+			items.push({ text: "今天产量和质量都在正常范围,没有需要单独过问的事项。", ok: true });
+		}
+
+		$("#attentionList").html(items.map(function (item) {
+			return '<li class="' + (item.ok ? "is-ok" : "") + '">' + escapeHtml(item.text) + "</li>";
+		}).join(""));
+	}
+
+	function renderBars(target, rows, emptyText) {
+		if (!rows || !rows.length) {
+			$("#" + target).html('<div class="empty-tip">' + escapeHtml(emptyText) + "</div>");
+			return;
+		}
+		var html = rows.map(function (row) {
+			var plan = num(row.plan);
+			var actual = num(row.actual);
+			var rate = plan > 0 ? Math.max(0, Math.min(num(row.achieve), 100)) : 0;
+			var cls = plan > 0 ? level(row.achieve) : "";
+			var meta = plan > 0 ? (actual + " / " + plan) : String(actual);
+			return '<div class="bar-row">' +
+				'<div class="bar-name">' + escapeHtml(row.title || "--") + "</div>" +
+				'<div class="track"><i class="' + cls + '" style="width:' + rate + '%"></i></div>' +
+				'<div class="bar-meta">' + escapeHtml(meta) + "</div>" +
+				"</div>";
+		}).join("");
+		$("#" + target).html(html);
+	}
+
+	function renderChart(trend) {
+		trend = trend || {};
+		if (!chart) {
+			chart = echarts.init(document.getElementById("outputChart"));
+		}
+		chart.setOption({
+			tooltip: {
+				trigger: "axis",
+				backgroundColor: "rgba(8, 24, 46, 0.94)",
+				borderColor: "#3d7eb8",
+				textStyle: { color: "#fff" }
+			},
+			legend: {
+				top: 0,
+				right: 8,
+				textStyle: { color: "#c5ddf2", fontSize: 12 },
+				data: ["计划", "完工"]
+			},
+			grid: { left: 48, right: 18, top: 32, bottom: 28 },
+			xAxis: {
+				type: "category",
+				data: trend.labels || [],
+				axisLine: { lineStyle: { color: "#245074" } },
+				axisTick: { show: false },
+				axisLabel: { color: "#c5ddf2" }
+			},
+			yAxis: {
+				type: "value",
+				minInterval: 1,
+				axisLine: { lineStyle: { color: "#245074" } },
+				axisTick: { show: false },
+				axisLabel: { color: "#c5ddf2" },
+				splitLine: { lineStyle: { color: "rgba(255,255,255,0.06)" } }
+			},
+			series: [
+				{
+					name: "计划",
+					type: "bar",
+					barWidth: 16,
+					data: trend.planQty || [],
+					itemStyle: { color: "rgba(127, 212, 255, 0.35)", normal: { color: "rgba(127, 212, 255, 0.35)" } }
+				},
+				{
+					name: "完工",
+					type: "bar",
+					barWidth: 16,
+					data: trend.output || [],
+					itemStyle: { color: "#3ec6ff", normal: { color: "#3ec6ff" } }
+				}
+			]
+		}, true);
+		setTimeout(function () {
+			if (chart) {
+				chart.resize();
+			}
+		}, 0);
+	}
+
+	function applyData(data) {
+		renderHero(data);
+		renderAttention(data);
+		renderChart(data.trend);
+		renderBars("productBars", data.products, "今天没有按产品拆开的计划");
+		renderBars("shiftBars", data.shifts, "今天没有班次数据");
+	}
+
+	function loadData() {
+		$.ajax({
+			url: config.apiUrl,
+			type: "GET",
+			dataType: "json",
+			timeout: 20000
+		}).done(function (res) {
+			if (res && (res.result === "true" || res.result === true) && res.data) {
+				applyData(res.data);
+				scheduleRefresh(config.refreshInterval || 60000);
+			} else {
+				scheduleRefresh(5000);
+			}
+		}).fail(function () {
+			scheduleRefresh(5000);
+		});
+	}
+
+	function scheduleRefresh(delay) {
+		if (refreshTimer) {
+			clearTimeout(refreshTimer);
+		}
+		refreshTimer = setTimeout(loadData, delay || 60000);
+	}
+
+	$(function () {
+		$("#screenTitle").text(config.title || "生产简报");
+		updateClock();
+		setInterval(updateClock, 1000);
+		loadData();
+		$(window).on("resize", function () {
+			if (chart) {
+				chart.resize();
+			}
+		});
+	});
+})(jQuery, window);

+ 275 - 0
src/main/resources/static/screen/kpi-screen.css

@@ -0,0 +1,275 @@
+html,
+body {
+	width: 100%;
+	height: 100%;
+	margin: 0;
+	overflow: hidden;
+	background: #061326;
+	font-family: "Microsoft YaHei", Arial, sans-serif;
+}
+
+.screen-page {
+	position: relative;
+	width: 1920px;
+	height: 1080px;
+	color: #fff;
+	overflow: hidden;
+}
+
+.screen-page .bg {
+	position: absolute;
+	inset: 0;
+	width: 100%;
+	height: 100%;
+	object-fit: cover;
+	z-index: 0;
+}
+
+.screen-content {
+	position: relative;
+	z-index: 1;
+	height: 100%;
+	padding: 18px 36px 16px;
+	box-sizing: border-box;
+	display: flex;
+	flex-direction: column;
+	gap: 12px;
+}
+
+.screen-header {
+	display: grid;
+	grid-template-columns: 1fr auto 1fr;
+	align-items: center;
+	height: 64px;
+	flex-shrink: 0;
+}
+
+.header-meta {
+	display: flex;
+	gap: 10px;
+}
+
+.meta-chip {
+	height: 32px;
+	padding: 0 14px;
+	border: 1px solid rgba(0, 143, 253, 0.4);
+	border-radius: 16px;
+	background: rgba(4, 24, 52, 0.72);
+	color: #9ad8ff;
+	font-size: 16px;
+	line-height: 32px;
+}
+
+.demo-toggle {
+	cursor: pointer;
+	font-family: inherit;
+}
+
+.demo-toggle.is-on {
+	color: #ffd56a;
+	border-color: rgba(255, 184, 0, 0.75);
+	background: rgba(92, 58, 0, 0.72);
+}
+
+.screen-title-new {
+	color: #fff;
+	font-size: 36px;
+	font-weight: 700;
+	letter-spacing: 6px;
+	text-align: center;
+	text-shadow: 0 0 18px rgba(0, 246, 255, 0.45);
+}
+
+.screen-time {
+	justify-self: end;
+	font-size: 20px;
+	color: #9ad8ff;
+}
+
+.summary-row {
+	display: grid;
+	grid-template-columns: repeat(6, 1fr);
+	gap: 12px;
+	flex-shrink: 0;
+}
+
+.summary-item {
+	height: 78px;
+	padding: 10px 16px;
+	border: 1px solid rgba(0, 143, 253, 0.34);
+	border-radius: 6px;
+	background: linear-gradient(180deg, rgba(5, 37, 78, 0.88), rgba(5, 25, 54, 0.76));
+	box-shadow: inset 0 0 24px rgba(0, 143, 253, 0.12);
+	box-sizing: border-box;
+}
+
+.summary-item.is-actual { border-color: rgba(0, 246, 255, 0.45); }
+.summary-item.is-ok { border-color: rgba(64, 222, 120, 0.45); }
+.summary-item.is-ng { border-color: rgba(255, 92, 92, 0.45); }
+.summary-item.is-repair { border-color: rgba(255, 184, 0, 0.45); }
+.summary-item.is-shift { border-color: rgba(120, 170, 255, 0.45); }
+
+.summary-label {
+	color: #9ad8ff;
+	font-size: 15px;
+}
+
+.summary-value {
+	margin-top: 4px;
+	font-size: 30px;
+	font-weight: 700;
+	line-height: 1.1;
+	font-family: Arial, sans-serif;
+}
+
+.summary-text {
+	font-size: 24px;
+	font-family: "Microsoft YaHei", sans-serif;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.kpi-row {
+	display: grid;
+	grid-template-columns: repeat(4, 1fr);
+	gap: 12px;
+	height: 168px;
+	flex-shrink: 0;
+}
+
+.kpi-card,
+.panel {
+	border: 1px solid rgba(0, 143, 253, 0.34);
+	border-radius: 6px;
+	background: linear-gradient(180deg, rgba(5, 32, 68, 0.82), rgba(4, 18, 40, 0.72));
+	box-shadow: inset 0 0 30px rgba(0, 143, 253, 0.1);
+	box-sizing: border-box;
+}
+
+.kpi-card {
+	display: flex;
+	align-items: center;
+	padding: 6px 12px 6px 0;
+	min-width: 0;
+}
+
+.kpi-chart {
+	width: 168px;
+	height: 150px;
+	flex-shrink: 0;
+}
+
+.kpi-side {
+	flex: 1;
+	min-width: 0;
+}
+
+.kpi-name {
+	font-size: 20px;
+	font-weight: 700;
+}
+
+.kpi-desc,
+.kpi-extra,
+.kpi-factor {
+	margin-top: 6px;
+	color: #9ad8ff;
+	font-size: 14px;
+}
+
+.kpi-factor {
+	display: flex;
+	justify-content: space-between;
+	padding-right: 8px;
+}
+
+.kpi-factor b {
+	color: #fff;
+	font-family: Arial, sans-serif;
+}
+
+.chart-row {
+	display: grid;
+	grid-template-columns: 1.15fr 0.85fr;
+	gap: 12px;
+	height: 280px;
+	flex-shrink: 0;
+}
+
+.panel {
+	display: flex;
+	flex-direction: column;
+	min-width: 0;
+	min-height: 0;
+	padding: 10px 14px 8px;
+}
+
+.panel-title {
+	height: 28px;
+	color: #d8f4ff;
+	font-size: 18px;
+	font-weight: 700;
+	flex-shrink: 0;
+}
+
+.chart-box {
+	flex: 1;
+	min-height: 0;
+}
+
+.table-row {
+	display: grid;
+	grid-template-columns: 1.45fr 0.7fr;
+	gap: 12px;
+	flex: 1;
+	min-height: 0;
+}
+
+.table-wrap {
+	flex: 1;
+	min-height: 0;
+	overflow: hidden;
+}
+
+.kpi-table {
+	width: 100%;
+	border-collapse: collapse;
+	table-layout: fixed;
+}
+
+.kpi-table th,
+.kpi-table td {
+	height: 36px;
+	padding: 0 8px;
+	text-align: center;
+	font-size: 15px;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.kpi-table th {
+	color: #7fd4ff;
+	font-weight: 600;
+	background: rgba(0, 143, 253, 0.12);
+}
+
+.kpi-table td {
+	color: #fff;
+	border-bottom: 1px solid rgba(0, 143, 253, 0.12);
+}
+
+.kpi-table tbody tr.is-current td {
+	background: rgba(64, 222, 120, 0.12);
+	color: #d9ffe6;
+}
+
+.kpi-table .is-good { color: #40de78; }
+.kpi-table .is-ok { color: #3ecbff; }
+.kpi-table .is-warn { color: #ffb800; }
+.kpi-table .is-bad { color: #ff5c5c; }
+
+.empty-row td {
+	color: #7aa8c8;
+}

+ 434 - 0
src/main/resources/static/screen/kpi-screen.js

@@ -0,0 +1,434 @@
+(function ($, window) {
+	"use strict";
+
+	var config = window.kpiScreenConfig || {};
+	var charts = {};
+	var refreshTimer = null;
+	var TEXT = "#d7ecff";
+	var AXIS = "#2A4E77";
+
+	function pad(num) {
+		return num < 10 ? "0" + num : String(num);
+	}
+
+	function updateClock() {
+		var now = new Date();
+		$("#screenTime").text(
+			now.getFullYear() + "-" + pad(now.getMonth() + 1) + "-" + pad(now.getDate()) +
+			" " + pad(now.getHours()) + ":" + pad(now.getMinutes()) + ":" + pad(now.getSeconds())
+		);
+	}
+
+	function escapeHtml(text) {
+		return String(text == null ? "" : text)
+			.replace(/&/g, "&amp;")
+			.replace(/</g, "&lt;")
+			.replace(/>/g, "&gt;")
+			.replace(/"/g, "&quot;");
+	}
+
+	function num(value) {
+		var n = Number(value);
+		return isNaN(n) ? 0 : n;
+	}
+
+	function rateText(value) {
+		return num(value).toFixed(1) + "%";
+	}
+
+	function rateClass(value) {
+		var n = num(value);
+		if (n >= 95) {
+			return "is-good";
+		}
+		if (n >= 85) {
+			return "is-ok";
+		}
+		if (n >= 70) {
+			return "is-warn";
+		}
+		return "is-bad";
+	}
+
+	function rateColor(value) {
+		var level = rateClass(value);
+		if (level === "is-good") {
+			return "#40de78";
+		}
+		if (level === "is-ok") {
+			return "#3ecbff";
+		}
+		if (level === "is-warn") {
+			return "#ffb800";
+		}
+		return "#ff5c5c";
+	}
+
+	function chartOf(id) {
+		if (!charts[id]) {
+			charts[id] = echarts.init(document.getElementById(id));
+		}
+		return charts[id];
+	}
+
+	function renderGauge(id, value, neutral) {
+		var raw = num(value);
+		var ratio = Math.max(0, Math.min(raw, 100)) / 100;
+		var color = neutral ? "#7aa8c8" : rateColor(raw);
+		chartOf(id).setOption({
+			series: [{
+				type: "gauge",
+				min: 0,
+				max: 100,
+				startAngle: 210,
+				endAngle: -30,
+				radius: "92%",
+				center: ["50%", "58%"],
+				pointer: { show: false },
+				axisTick: { show: false },
+				splitLine: { show: false },
+				axisLabel: { show: false },
+				axisLine: {
+					lineStyle: {
+						width: 12,
+						color: [[ratio, color], [1, "rgba(255,255,255,0.12)"]]
+					}
+				},
+				detail: {
+					offsetCenter: [0, "6%"],
+					color: "#fff",
+					fontSize: 26,
+					fontWeight: 700,
+					formatter: function () {
+						return raw.toFixed(1) + "%";
+					}
+				},
+				data: [{ value: raw }]
+			}]
+		}, true);
+	}
+
+	function renderSummary(data) {
+		var summary = data.summary || {};
+		var kpi = data.kpi || {};
+		$("#statDay").text("统计日 " + (data.day || "--"));
+		$("#statWindow").text(data.window || "08:00-次日08:00");
+		$("#planCount").text(summary.plan || 0);
+		$("#actualCount").text(summary.actual || 0);
+		$("#okCount").text(summary.ok || 0);
+		$("#ngCount").text(summary.ng || 0);
+		$("#repairCount").text(summary.repair || 0);
+		$("#shiftTitle").text(data.shiftTitle || "未排班");
+		$("#achieveExtra").text(summary.plan ? ((summary.actual || 0) + " / " + summary.plan) : "未排班次计划");
+		$("#fpyExtra").text("合格 " + (summary.ok || 0) + " / 检验 " + ((summary.ok || 0) + (summary.ng || 0)));
+		$("#availabilityRate").text(rateText(kpi.availability));
+		$("#performanceRate").text(rateText(kpi.performance));
+		$("#qualityRate").text(rateText(kpi.quality));
+		$("#shiftDesc").text((data.shiftTitle || "当前班次") + "报工达成");
+		$("#alarmExtra").text("未关闭报警 " + (summary.openAlarm || 0));
+		renderGauge("achieveGauge", kpi.achieve, !summary.plan);
+		renderGauge("fpyGauge", kpi.fpy, !(summary.ok || summary.ng || summary.actual));
+		renderGauge("oeeGauge", kpi.oee, !summary.plan && !(summary.ok || summary.ng));
+		renderGauge("shiftGauge", kpi.shiftAchieve, !summary.plan);
+	}
+
+	function renderTrend(trend) {
+		trend = trend || {};
+		chartOf("trendChart").setOption({
+			tooltip: {
+				trigger: "axis",
+				backgroundColor: "rgba(8, 28, 58, 0.92)",
+				borderColor: "#008FFD",
+				textStyle: { color: "#fff" }
+			},
+			legend: {
+				top: 0,
+				right: 8,
+				textStyle: { color: TEXT, fontSize: 13 },
+				data: ["达成率", "直通率", "OEE"]
+			},
+			grid: { left: 46, right: 18, top: 36, bottom: 28 },
+			xAxis: {
+				type: "category",
+				data: trend.labels || [],
+				axisLine: { lineStyle: { color: AXIS } },
+				axisTick: { show: false },
+				axisLabel: { color: TEXT, fontSize: 13 }
+			},
+			yAxis: {
+				type: "value",
+				min: 0,
+				max: 100,
+				axisLine: { lineStyle: { color: AXIS } },
+				axisTick: { show: false },
+				axisLabel: { color: TEXT, formatter: "{value}%" },
+				splitLine: { lineStyle: { color: "rgba(255,255,255,0.08)" } }
+			},
+			series: [
+				lineSeries("达成率", trend.achieve, "#3ecbff"),
+				lineSeries("直通率", trend.fpy, "#40de78"),
+				lineSeries("OEE", trend.oee, "#ffb800")
+			]
+		}, true);
+	}
+
+	function lineSeries(name, data, color) {
+		return {
+			name: name,
+			type: "line",
+			smooth: true,
+			symbol: "circle",
+			symbolSize: 7,
+			data: data || [],
+			itemStyle: { color: color },
+			lineStyle: { width: 2, color: color }
+		};
+	}
+
+	function renderHours(hours) {
+		hours = hours || {};
+		chartOf("hourChart").setOption({
+			tooltip: {
+				trigger: "axis",
+				backgroundColor: "rgba(8, 28, 58, 0.92)",
+				borderColor: "#008FFD",
+				textStyle: { color: "#fff" }
+			},
+			legend: {
+				top: 0,
+				right: 8,
+				textStyle: { color: TEXT, fontSize: 13 },
+				data: ["完工", "节拍目标"]
+			},
+			grid: { left: 42, right: 12, top: 36, bottom: 28 },
+			xAxis: {
+				type: "category",
+				data: hours.labels || [],
+				axisLine: { lineStyle: { color: AXIS } },
+				axisTick: { show: false },
+				axisLabel: { color: TEXT, fontSize: 12 }
+			},
+			yAxis: {
+				type: "value",
+				minInterval: 1,
+				axisLine: { lineStyle: { color: AXIS } },
+				axisTick: { show: false },
+				axisLabel: { color: TEXT },
+				splitLine: { lineStyle: { color: "rgba(255,255,255,0.08)" } }
+			},
+			series: [
+				{
+					name: "完工",
+					type: "bar",
+					barWidth: 16,
+					data: hours.actual || [],
+					itemStyle: {
+						color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+							{ offset: 0, color: "#49e0ff" },
+							{ offset: 1, color: "#0b6cff" }
+						])
+					}
+				},
+				{
+					name: "节拍目标",
+					type: "line",
+					data: hours.plan || [],
+					symbol: "none",
+					itemStyle: { color: "#ffb800" },
+					lineStyle: { type: "dashed", width: 2, color: "#ffb800" }
+				}
+			]
+		}, true);
+	}
+
+	function renderShifts(rows) {
+		var html = "";
+		if (!rows || !rows.length) {
+			html = '<tr class="empty-row"><td colspan="9">暂无班次报工</td></tr>';
+		} else {
+			rows.forEach(function (row) {
+				html += '<tr class="' + (row.current ? "is-current" : "") + '">' +
+					"<td>" + escapeHtml(row.title) + (row.current ? " · 当前" : "") + "</td>" +
+					"<td>" + escapeHtml(row.range) + "</td>" +
+					"<td>" + num(row.plan) + "</td>" +
+					"<td>" + num(row.actual) + "</td>" +
+					"<td>" + num(row.ok) + "</td>" +
+					"<td>" + num(row.ng) + "</td>" +
+					"<td>" + num(row.repair) + "</td>" +
+					'<td class="' + rateClass(row.achieve) + '">' + rateText(row.achieve) + "</td>" +
+					'<td class="' + rateClass(row.fpy) + '">' + rateText(row.fpy) + "</td>" +
+					"</tr>";
+			});
+		}
+		$("#shiftBody").html(html);
+	}
+
+	function renderProducts(rows) {
+		var html = "";
+		if (!rows || !rows.length) {
+			html = '<tr class="empty-row"><td colspan="4">暂无产品计划</td></tr>';
+		} else {
+			rows.forEach(function (row) {
+				html += "<tr>" +
+					"<td>" + escapeHtml(row.title) + "</td>" +
+					"<td>" + num(row.plan) + "</td>" +
+					"<td>" + num(row.actual) + "</td>" +
+					'<td class="' + rateClass(row.achieve) + '">' + rateText(row.achieve) + "</td>" +
+					"</tr>";
+			});
+		}
+		$("#productBody").html(html);
+	}
+
+	function applyData(data) {
+		renderSummary(data);
+		renderTrend(data.trend);
+		renderHours(data.hours);
+		renderShifts(data.shifts);
+		renderProducts(data.products);
+	}
+
+	var demoOn = false;
+
+	function statDayText(date) {
+		var day = new Date(date.getTime());
+		if (day.getHours() < 8) {
+			day.setDate(day.getDate() - 1);
+		}
+		return day.getFullYear() + "-" + pad(day.getMonth() + 1) + "-" + pad(day.getDate());
+	}
+
+	function trendLabel(date, offset) {
+		var day = new Date(date.getTime());
+		if (day.getHours() < 8) {
+			day.setDate(day.getDate() - 1);
+		}
+		day.setDate(day.getDate() - offset);
+		return (day.getMonth() + 1) + "/" + day.getDate();
+	}
+
+	function buildDemoData() {
+		var now = new Date();
+		var labels = [];
+		for (var i = 6; i >= 0; i--) {
+			labels.push(trendLabel(now, i));
+		}
+		return {
+			day: statDayText(now),
+			window: "08:00-次日08:00",
+			shiftTitle: "白班",
+			summary: { plan: 480, actual: 436, ok: 860, ng: 28, repair: 11, openAlarm: 1 },
+			kpi: {
+				achieve: 90.8,
+				fpy: 96.8,
+				oee: 85.4,
+				availability: 97.2,
+				performance: 90.8,
+				quality: 96.8,
+				shiftAchieve: 94.2
+			},
+			hours: {
+				labels: ["08:00", "10:00", "12:00", "14:00", "16:00", "18:00", "20:00", "22:00", "00:00", "02:00", "04:00", "06:00"],
+				actual: [28, 42, 38, 44, 46, 47, 36, 40, 38, 34, 28, 15],
+				plan: [40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40]
+			},
+			trend: {
+				labels: labels,
+				achieve: [88.2, 91.0, 86.4, 93.1, 89.5, 92.4, 90.8],
+				fpy: [96.1, 97.2, 95.4, 97.8, 96.6, 97.0, 96.8],
+				oee: [82.1, 85.4, 80.6, 88.2, 84.0, 86.7, 85.4]
+			},
+			shifts: [
+				{ title: "白班", range: "08:00-20:00", plan: 260, actual: 245, ok: 470, ng: 12, repair: 5, achieve: 94.2, fpy: 97.5, current: true },
+				{ title: "夜班", range: "20:00-08:00", plan: 220, actual: 191, ok: 390, ng: 16, repair: 6, achieve: 86.8, fpy: 96.1, current: false }
+			],
+			products: [
+				{ title: "冷板总成", plan: 180, actual: 172, achieve: 95.6 },
+				{ title: "框架总成", plan: 160, actual: 146, achieve: 91.3 },
+				{ title: "箱体总成", plan: 140, actual: 118, achieve: 84.3 }
+			]
+		};
+	}
+
+	function syncDemoButton() {
+		$("#demoToggle").toggleClass("is-on", demoOn).text(demoOn ? "退出演示" : "演示模式");
+	}
+
+	function setDemo(on) {
+		demoOn = !!on;
+		syncDemoButton();
+		if (refreshTimer) {
+			clearTimeout(refreshTimer);
+			refreshTimer = null;
+		}
+		if (demoOn) {
+			applyData(buildDemoData());
+			return;
+		}
+		loadData();
+	}
+
+	function loadData() {
+		if (demoOn) {
+			return;
+		}
+		$.ajax({
+			url: config.apiUrl,
+			type: "GET",
+			dataType: "json",
+			timeout: 20000
+		}).done(function (res) {
+			if (demoOn) {
+				return;
+			}
+			if (res && (res.result === "true" || res.result === true) && res.data) {
+				applyData(res.data);
+				scheduleRefresh(config.refreshInterval || 60000);
+			} else {
+				scheduleRefresh(5000);
+			}
+		}).fail(function () {
+			if (!demoOn) {
+				scheduleRefresh(5000);
+			}
+		});
+	}
+
+	function scheduleRefresh(delay) {
+		if (refreshTimer) {
+			clearTimeout(refreshTimer);
+		}
+		refreshTimer = setTimeout(loadData, delay || 60000);
+	}
+
+	function fitScreen() {
+		var width = $(window).width();
+		var height = $(window).height();
+		var scale = Math.min(width / 1920, height / 1080);
+		$("body").css({
+			transform: "scale(" + scale + ")",
+			transformOrigin: "0 0",
+			width: "1920px",
+			height: "1080px",
+			overflow: "hidden",
+			marginLeft: ((width - 1920 * scale) / 2) + "px",
+			marginTop: ((height - 1080 * scale) / 2) + "px"
+		});
+		Object.keys(charts).forEach(function (key) {
+			charts[key].resize();
+		});
+	}
+
+	$(function () {
+		$("#screenTitle").text(config.title || "生产运营看板");
+		updateClock();
+		setInterval(updateClock, 1000);
+		$("#demoToggle").on("click", function () {
+			setDemo(!demoOn);
+		});
+		var query = window.location.search || "";
+		setDemo(/(?:^|[?&])demo=1(?:&|$)/.test(query));
+		fitScreen();
+		$(window).on("resize", fitScreen);
+	});
+})(jQuery, window);

+ 56 - 0
src/main/resources/static/screen/screen-fullscreen.js

@@ -0,0 +1,56 @@
+(function () {
+	var style = document.createElement("style");
+	style.textContent = [
+		".screen-fullscreen-btn{position:fixed;right:18px;bottom:18px;z-index:99999;",
+		"height:42px;padding:0 16px 0 14px;border:1px solid rgba(140,198,255,.62);",
+		"border-radius:21px;background:rgba(6,22,46,.86);color:#e7f4ff;",
+		"font-size:15px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;gap:8px;",
+		"box-shadow:0 6px 18px rgba(0,0,0,.28);font-family:Microsoft YaHei,Arial,sans-serif;}",
+		".screen-fullscreen-btn:hover{background:rgba(18,58,108,.94);}",
+		".screen-fullscreen-btn svg{width:16px;height:16px;display:block;fill:none;stroke:currentColor;stroke-width:1.8;}"
+	].join("");
+	document.head.appendChild(style);
+
+	var btn = document.createElement("button");
+	btn.type = "button";
+	btn.className = "screen-fullscreen-btn";
+	btn.innerHTML = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 6V2h4M10 2h4v4M14 10v4h-4M6 14H2v-4"/></svg><span class="fs-label">全屏</span>';
+	document.body.appendChild(btn);
+
+	function current() {
+		return document.fullscreenElement || document.webkitFullscreenElement || null;
+	}
+
+	function sync() {
+		var on = !!current();
+		var label = btn.querySelector(".fs-label");
+		label.textContent = on ? "退出全屏" : "全屏";
+		btn.title = on ? "退出全屏" : "全屏显示";
+	}
+
+	function enter() {
+		var el = document.documentElement;
+		var req = el.requestFullscreen || el.webkitRequestFullscreen;
+		if (req) {
+			req.call(el);
+		}
+	}
+
+	function exit() {
+		var leave = document.exitFullscreen || document.webkitExitFullscreen;
+		if (leave) {
+			leave.call(document);
+		}
+	}
+
+	btn.addEventListener("click", function () {
+		if (current()) {
+			exit();
+		} else {
+			enter();
+		}
+	});
+	document.addEventListener("fullscreenchange", sync);
+	document.addEventListener("webkitfullscreenchange", sync);
+	sync();
+})();

+ 17 - 5
src/main/resources/views/modules/mes/mesMaterialCheckRecordList.html

@@ -64,11 +64,23 @@ $('#btnExport').click(function(){
 		js.showMessage('${text("请先勾选要导出的来料检查记录")}');
 		return;
 	}
-	js.ajaxSubmit({
-		url:'${ctx}/mes/mesMaterialCheckRecord/exportData',
-		data:{ids: ids.join(',')},
-		downloadFile:true
-	});
+	// 不用 ajax:jeesite 会把 xlsx 当 JSON 解析,并把文件内容当成报错弹出来
+	var frameName = 'materialCheckExportFrame';
+	if ($('iframe[name="' + frameName + '"]').length === 0) {
+		$('body').append('<iframe name="' + frameName + '" style="display:none;"></iframe>');
+	}
+	var $form = $('<form method="post" style="display:none;"></form>');
+	$form.attr('action', '${ctx}/mes/mesMaterialCheckRecord/exportData');
+	$form.attr('target', frameName);
+	$form.append($('<input type="hidden" name="ids"/>').val(ids.join(',')));
+	var csrfParam = $('meta[name="csrf-param"]').attr('content');
+	var csrfToken = $('meta[name="csrf-token"]').attr('content');
+	if (csrfParam && csrfToken) {
+		$form.append($('<input type="hidden"/>').attr('name', csrfParam).val(csrfToken));
+	}
+	$('body').append($form);
+	$form[0].submit();
+	setTimeout(function(){ $form.remove(); }, 2000);
 });
 // 初始化DataGrid对象
 $('#dataGrid').dataGrid({

+ 151 - 0
src/main/resources/views/modules/mes/mesScreen10.html

@@ -0,0 +1,151 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<meta http-equiv="X-UA-Compatible" content="ie=edge">
+	<title>生产运营看板</title>
+	<link rel="stylesheet" href="${ctxStatic}/screen/kpi-screen.css?v=20261010">
+</head>
+<body>
+<div class="screen-page">
+	<img class="bg" src="${ctxStatic}/screen/imgs/bg3.png" alt="">
+	<div class="screen-content">
+		<div class="screen-header">
+			<div class="header-meta">
+				<span class="meta-chip" id="statDay">统计日 --</span>
+				<span class="meta-chip" id="statWindow">08:00-次日08:00</span>
+				<button type="button" class="meta-chip demo-toggle" id="demoToggle">演示模式</button>
+			</div>
+			<div class="screen-title-new" id="screenTitle">生产运营看板</div>
+			<div class="screen-time" id="screenTime"></div>
+		</div>
+
+		<div class="summary-row">
+			<div class="summary-item">
+				<div class="summary-label">计划产量</div>
+				<div class="summary-value" id="planCount">0</div>
+			</div>
+			<div class="summary-item is-actual">
+				<div class="summary-label">实际完工</div>
+				<div class="summary-value" id="actualCount">0</div>
+			</div>
+			<div class="summary-item is-ok">
+				<div class="summary-label">检验合格</div>
+				<div class="summary-value" id="okCount">0</div>
+			</div>
+			<div class="summary-item is-ng">
+				<div class="summary-label">检验不良</div>
+				<div class="summary-value" id="ngCount">0</div>
+			</div>
+			<div class="summary-item is-repair">
+				<div class="summary-label">返修件数</div>
+				<div class="summary-value" id="repairCount">0</div>
+			</div>
+			<div class="summary-item is-shift">
+				<div class="summary-label">当前班次</div>
+				<div class="summary-value summary-text" id="shiftTitle">--</div>
+			</div>
+		</div>
+
+		<div class="kpi-row">
+			<div class="kpi-card">
+				<div class="kpi-chart" id="achieveGauge"></div>
+				<div class="kpi-side">
+					<div class="kpi-name">生产达成率</div>
+					<div class="kpi-desc">完工 / 班次计划</div>
+					<div class="kpi-extra" id="achieveExtra">0 / 0</div>
+				</div>
+			</div>
+			<div class="kpi-card">
+				<div class="kpi-chart" id="fpyGauge"></div>
+				<div class="kpi-side">
+					<div class="kpi-name">直通率</div>
+					<div class="kpi-desc">工位一次合格占比</div>
+					<div class="kpi-extra" id="fpyExtra">合格 0 / 检验 0</div>
+				</div>
+			</div>
+			<div class="kpi-card">
+				<div class="kpi-chart" id="oeeGauge"></div>
+				<div class="kpi-side">
+					<div class="kpi-name">OEE</div>
+					<div class="kpi-factor"><span>时间稼动</span><b id="availabilityRate">0%</b></div>
+					<div class="kpi-factor"><span>性能稼动</span><b id="performanceRate">0%</b></div>
+					<div class="kpi-factor"><span>合格率</span><b id="qualityRate">0%</b></div>
+				</div>
+			</div>
+			<div class="kpi-card">
+				<div class="kpi-chart" id="shiftGauge"></div>
+				<div class="kpi-side">
+					<div class="kpi-name">班次达成</div>
+					<div class="kpi-desc" id="shiftDesc">当前班次报工</div>
+					<div class="kpi-extra" id="alarmExtra">未关闭报警 0</div>
+				</div>
+			</div>
+		</div>
+
+		<div class="chart-row">
+			<div class="panel">
+				<div class="panel-title">近七日达成率 / 直通率 / OEE</div>
+				<div class="chart-box" id="trendChart"></div>
+			</div>
+			<div class="panel">
+				<div class="panel-title">当日两小时完工</div>
+				<div class="chart-box" id="hourChart"></div>
+			</div>
+		</div>
+
+		<div class="table-row">
+			<div class="panel table-panel">
+				<div class="panel-title">班次报工</div>
+				<div class="table-wrap">
+					<table class="kpi-table">
+						<thead>
+						<tr>
+							<th>班次</th>
+							<th>时段</th>
+							<th>计划</th>
+							<th>完工</th>
+							<th>合格</th>
+							<th>不良</th>
+							<th>返修</th>
+							<th>达成率</th>
+							<th>直通率</th>
+						</tr>
+						</thead>
+						<tbody id="shiftBody"></tbody>
+					</table>
+				</div>
+			</div>
+			<div class="panel table-panel product-panel">
+				<div class="panel-title">产品达成</div>
+				<div class="table-wrap">
+					<table class="kpi-table">
+						<thead>
+						<tr>
+							<th>产品</th>
+							<th>计划</th>
+							<th>完工</th>
+							<th>达成率</th>
+						</tr>
+						</thead>
+						<tbody id="productBody"></tbody>
+					</table>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+<script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
+<script src="${ctxStatic}/echarts/4.2/echarts.min.js"></script>
+<script>
+	window.kpiScreenConfig = {
+		title: "生产运营看板",
+		refreshInterval: 60000,
+		apiUrl: "${ctx}/mes/mesProduct/screenKpiData"
+	};
+</script>
+<script src="${ctxStatic}/screen/kpi-screen.js?v=20261010"></script>
+<script src="${ctxStatic}/screen/screen-fullscreen.js?v=20261010"></script>
+</body>
+</html>

+ 84 - 0
src/main/resources/views/modules/mes/mesScreen11.html

@@ -0,0 +1,84 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<meta http-equiv="X-UA-Compatible" content="ie=edge">
+	<title>生产简报</title>
+	<link rel="stylesheet" href="${ctxStatic}/screen/exec-screen.css?v=202609303">
+</head>
+<body>
+<div class="exec-page">
+	<header class="exec-header">
+		<div class="header-side">
+			<span class="meta-chip" id="statDay">统计日 --</span>
+			<span class="meta-chip" id="shiftChip">当前班次 --</span>
+		</div>
+		<h1 id="screenTitle">生产简报</h1>
+		<div class="header-side is-right">
+			<span class="screen-time" id="screenTime"></span>
+		</div>
+	</header>
+
+	<section class="hero-grid">
+		<article class="hero-card">
+			<div class="hero-label">今日完工</div>
+			<div class="hero-num" id="actualNum">0</div>
+			<div class="hero-sub" id="planText">计划 0</div>
+		</article>
+		<article class="hero-card">
+			<div class="hero-label" id="gapLabel">还差</div>
+			<div class="hero-num" id="gapNum">0</div>
+			<div class="hero-foot">
+				<em class="tag" id="achieveTag">--</em>
+				<span id="achieveText">达成 --</span>
+			</div>
+			<div class="meter"><i id="achieveBar"></i></div>
+		</article>
+		<article class="hero-card">
+			<div class="hero-label">一次合格</div>
+			<div class="hero-num" id="fpyNum">--</div>
+			<div class="hero-sub" id="qualityText">返修 0 · 不良 0</div>
+		</article>
+		<article class="hero-card">
+			<div class="hero-label">近七日完工</div>
+			<div class="hero-num" id="weekActual">0</div>
+			<div class="hero-sub" id="weekText">计划 0 · 平均达成 --</div>
+		</article>
+	</section>
+
+	<section class="main-grid">
+		<div class="panel chart-panel">
+			<div class="panel-title">近七日产量</div>
+			<div class="chart-box" id="outputChart"></div>
+		</div>
+		<div class="panel">
+			<div class="panel-title">需要过问</div>
+			<ul class="attention-list" id="attentionList"></ul>
+		</div>
+	</section>
+
+	<section class="bottom-grid">
+		<div class="panel">
+			<div class="panel-title">产品完成情况</div>
+			<div class="bar-list" id="productBars"></div>
+		</div>
+		<div class="panel">
+			<div class="panel-title">班次完成情况</div>
+			<div class="bar-list" id="shiftBars"></div>
+		</div>
+	</section>
+</div>
+<script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
+<script src="${ctxStatic}/echarts/4.2/echarts.min.js"></script>
+<script>
+	window.execScreenConfig = {
+		title: "生产简报",
+		refreshInterval: 60000,
+		apiUrl: "${ctx}/mes/mesProduct/screenKpiData"
+	};
+</script>
+<script src="${ctxStatic}/screen/exec-screen.js?v=202609303"></script>
+<script src="${ctxStatic}/screen/screen-fullscreen.js?v=20261010"></script>
+</body>
+</html>

File diff suppressed because it is too large
+ 385 - 960
src/main/resources/views/modules/mes/mesScreen4.html


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

@@ -187,5 +187,6 @@
 	};
 </script>
 <script src="${ctxStatic}/screen/oprno-record-screen.js?v=20250731"></script>
+<script src="${ctxStatic}/screen/screen-fullscreen.js?v=20261010"></script>
 </body>
 </html>

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

@@ -187,5 +187,6 @@
 	};
 </script>
 <script src="${ctxStatic}/screen/oprno-record-screen.js?v=20250731"></script>
+<script src="${ctxStatic}/screen/screen-fullscreen.js?v=20261010"></script>
 </body>
 </html>

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

@@ -187,5 +187,6 @@
 	};
 </script>
 <script src="${ctxStatic}/screen/oprno-record-screen.js?v=20250731"></script>
+<script src="${ctxStatic}/screen/screen-fullscreen.js?v=20261010"></script>
 </body>
 </html>

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

@@ -50,5 +50,6 @@
 	};
 </script>
 <script src="${ctxStatic}/screen/gp12-issue-screen.js?v=202607153"></script>
+<script src="${ctxStatic}/screen/screen-fullscreen.js?v=20261010"></script>
 </body>
 </html>

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

@@ -71,5 +71,6 @@
 	};
 </script>
 <script src="${ctxStatic}/screen/repair-screen.js?v=202607264"></script>
+<script src="${ctxStatic}/screen/screen-fullscreen.js?v=20261010"></script>
 </body>
 </html>

+ 87 - 0
src/main/resources/views/modules/mes/mesScreenForm.html

@@ -0,0 +1,87 @@
+<% 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(mesScreen.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="${mesScreen}" action="${ctx}/mes/mesScreen/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">
+								<span class="required">*</span> ${text('名称')}:</label>
+							<div class="col-sm-6">
+								<#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">
+								<span class="required">*</span> ${text('地址')}:</label>
+							<div class="col-sm-6">
+								<#form:input path="screenUrl" maxlength="255" class="form-control required" placeholder="/mes/mesProduct/screen10"/>
+								<div class="help-block">系统内页面填 /mes/mesProduct/screen10 这种相对地址,外部页面填完整网址。</div>
+							</div>
+						</div>
+					</div>
+				</div>
+				<div class="row">
+					<div class="col-xs-12">
+						<div class="form-group">
+							<label class="control-label col-sm-2">${text('说明')}:</label>
+							<div class="col-sm-6">
+								<#form:input path="remark" maxlength="255" 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">${text('排序')}:</label>
+							<div class="col-sm-2">
+								<#form:input path="sortNo" maxlength="8" class="form-control digits"/>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div class="box-footer">
+				<div class="row">
+					<div class="col-sm-offset-2 col-sm-10">
+						<% if (hasPermi('mes:mesScreen: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>

+ 71 - 0
src/main/resources/views/modules/mes/mesScreenList.html

@@ -0,0 +1,71 @@
+<% 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 fa-desktop"></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:mesScreen:edit')){ %>
+					<a href="${ctx}/mes/mesScreen/form" class="btn btn-default btnTool" title="${text('新增大屏')}"><i class="fa fa-plus"></i> ${text('新增')}</a>
+				<% } %>
+			</div>
+		</div>
+		<div class="box-body">
+			<#form:form id="searchForm" model="${mesScreen}" action="${ctx}/mes/mesScreen/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">
+					<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>
+function screenOpenUrl(url) {
+	if (!url) {
+		return "";
+	}
+	if (/^https?:\/\//i.test(url)) {
+		return url;
+	}
+	if (url.indexOf("/") === 0) {
+		return "${ctx}" + url;
+	}
+	return "${ctx}/" + url;
+}
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("名称")}', name:'title', index:'a.title', width:180, align:"left", frozen:true, formatter: function(val, obj, row, act){
+			return '<a href="${ctx}/mes/mesScreen/form?id='+row.id+'" class="btnList" data-title="${text("编辑大屏")}">'+(val||row.id)+'</a>';
+		}},
+		{header:'${text("地址")}', name:'screenUrl', index:'a.screen_url', width:280, align:"left"},
+		{header:'${text("说明")}', name:'remark', index:'a.remark', width:280, align:"left"},
+		{header:'${text("排序")}', name:'sortNo', index:'a.sort_no', width:70, align:"center"},
+		{header:'${text("操作")}', name:'actions', width:140, align:"center", formatter: function(val, obj, row, act){
+			var actions = [];
+			actions.push('<a href="'+screenOpenUrl(row.screenUrl)+'" class="btnList" target="_blank" title="${text("打开大屏")}"><i class="fa fa-desktop"></i></a>&nbsp;');
+			//<% if(hasPermi('mes:mesScreen:edit')){ %>
+				actions.push('<a href="${ctx}/mes/mesScreen/form?id='+row.id+'" class="btnList" title="${text("编辑大屏")}"><i class="fa fa-pencil"></i></a>&nbsp;');
+				actions.push('<a href="${ctx}/mes/mesScreen/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>

+ 187 - 0
src/main/resources/views/modules/mes/mesWorkOrderForm.html

@@ -0,0 +1,187 @@
+<% 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(mesWorkOrder.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>
+		<%
+		var stateLabel = '待下达';
+		if(mesWorkOrder.state == '1'){
+			stateLabel = '已下达';
+		}else if(mesWorkOrder.state == '2'){
+			stateLabel = '生产中';
+		}else if(mesWorkOrder.state == '3'){
+			stateLabel = '完工';
+		}else if(mesWorkOrder.state == '4'){
+			stateLabel = '暂停';
+		}else if(mesWorkOrder.state == '5'){
+			stateLabel = '关闭';
+		}
+		%>
+		<#form:form id="inputForm" model="${mesWorkOrder}" action="${ctx}/mes/mesWorkOrder/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">
+								<span class="required">*</span> ${text('工单号')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="workNo" maxlength="64" class="form-control required" placeholder="M开头"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('项目别')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="projectCode" maxlength="64" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								<span class="required">*</span> ${text('总成料号')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="materialSn" maxlength="64" class="form-control required" placeholder="1开头"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('规格名称')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="specName" maxlength="255" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('图号')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="drawingNo" maxlength="128" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								<span class="required">*</span> ${text('需生产数量')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="planQty" maxlength="8" class="form-control required digits"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								<span class="required">*</span> ${text('开单日期')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="openDate" readonly="true" maxlength="20" class="form-control laydate required"
+									dataFormat="date" data-type="date" data-format="yyyy-MM-dd"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								<span class="required">*</span> ${text('生产日期')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="produceDate" readonly="true" maxlength="20" class="form-control laydate required"
+									dataFormat="date" data-type="date" data-format="yyyy-MM-dd"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('订单编号')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="saleOrderNo" maxlength="64" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('产线')}:</label>
+							<div class="col-sm-8">
+								<#form:select path="lineSn" items="${lines}" itemLabel="title" itemValue="sn" blankOption="true" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('工件类型')}:</label>
+							<div class="col-sm-8">
+								<#form:select path="cateSn" items="${cates}" itemLabel="title" itemValue="sn" blankOption="true" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('状态')}:</label>
+							<div class="col-sm-8">
+								<input type="text" class="form-control" readonly value="${stateLabel}"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('备注')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="remarks" 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:mesWorkOrder: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>
+<% if(!mesWorkOrder.isNewRecord && mesWorkOrder.state != '0'){ %>
+	$('#workNo,#projectCode,#materialSn,#specName,#drawingNo,#planQty,#cateSn').attr('readonly', 'readonly');
+	$('#cateSn').attr('disabled', 'disabled');
+<% } %>
+<% if(!mesWorkOrder.isNewRecord && mesWorkOrder.state != '0' && mesWorkOrder.state != '1'){ %>
+	$('#inputForm').find('input,select,textarea').attr('readonly', 'readonly').attr('disabled', 'disabled');
+	$('#btnSubmit').hide();
+<% } %>
+$("#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>

+ 190 - 0
src/main/resources/views/modules/mes/mesWorkOrderList.html

@@ -0,0 +1,190 @@
+<% 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> ${text('导出')}</a>
+				<% if(hasPermi('mes:mesWorkOrder:edit')){ %>
+					<a href="#" class="btn btn-default" id="btnImport"><i class="glyphicon glyphicon-import"></i> ${text('导入')}</a>
+					<a href="${ctx}/mes/mesWorkOrder/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="${mesWorkOrder}" action="${ctx}/mes/mesWorkOrder/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="workNo" maxlength="64" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('总成料号')}:</label>
+					<div class="control-inline">
+						<#form:input path="materialSn" maxlength="64" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('项目别')}:</label>
+					<div class="control-inline">
+						<#form:input path="projectCode" maxlength="64" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('订单编号')}:</label>
+					<div class="control-inline">
+						<#form:input path="saleOrderNo" maxlength="64" 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="lineSn" items="${lines}" itemLabel="title" itemValue="sn" blankOption="true" class="form-control"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('工件类型')}:</label>
+					<div class="control-inline width-120">
+						<#form:select path="cateSn" items="${cates}" itemLabel="title" itemValue="sn" blankOption="true" class="form-control"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('状态')}:</label>
+					<div class="control-inline width-120">
+						<select name="state" class="form-control">
+							<option value="">${text('全部')}</option>
+							<option value="0"<% if(mesWorkOrder.state == '0'){ %> selected<% } %>>${text('待下达')}</option>
+							<option value="1"<% if(mesWorkOrder.state == '1'){ %> selected<% } %>>${text('已下达')}</option>
+							<option value="2"<% if(mesWorkOrder.state == '2'){ %> selected<% } %>>${text('生产中')}</option>
+							<option value="3"<% if(mesWorkOrder.state == '3'){ %> selected<% } %>>${text('完工')}</option>
+							<option value="4"<% if(mesWorkOrder.state == '4'){ %> selected<% } %>>${text('暂停')}</option>
+							<option value="5"<% if(mesWorkOrder.state == '5'){ %> selected<% } %>>${text('关闭')}</option>
+						</select>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('生产日期')}:</label>
+					<div class="control-inline">
+						<#form:input path="produceDate_gte" readonly="true" maxlength="20" class="form-control laydate width-date"
+							dataFormat="date" data-type="date" data-format="yyyy-MM-dd"/>
+						&nbsp;-&nbsp;
+						<#form:input path="produceDate_lte" readonly="true" maxlength="20" class="form-control laydate width-date"
+							dataFormat="date" data-type="date" data-format="yyyy-MM-dd"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<button type="submit" class="btn btn-primary btn-sm">${text('查询')}</button>
+					<button type="reset" class="btn btn-default btn-sm">${text('重置')}</button>
+				</div>
+			</#form:form>
+			<table id="dataGrid"></table>
+			<div id="dataGridPage"></div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+var workOrderStateMap = {'0':'待下达','1':'已下达','2':'生产中','3':'完工','4':'暂停','5':'关闭'};
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("工单号")}', name:'workNo', index:'a.work_no', width:150, align:"center", frozen:true, formatter: function(val, obj, row, act){
+			return '<a href="${ctx}/mes/mesWorkOrder/form?id='+row.id+'" class="btnList" data-title="${text("编辑工单")}">'+(val||'')+'</a>';
+		}},
+		{header:'${text("项目别")}', name:'projectCode', index:'a.project_code', width:90, align:"center"},
+		{header:'${text("总成料号")}', name:'materialSn', index:'a.material_sn', width:140, align:"center"},
+		{header:'${text("规格名称")}', name:'specName', index:'a.spec_name', width:160, align:"left"},
+		{header:'${text("图号")}', name:'drawingNo', index:'a.drawing_no', width:120, align:"center"},
+		{header:'${text("需生产数量")}', name:'planQty', index:'a.plan_qty', width:100, align:"center"},
+		{header:'${text("已完工")}', name:'doneQty', index:'a.done_qty', width:80, align:"center"},
+		{header:'${text("开单日期")}', name:'openDate', index:'a.open_date', width:110, align:"center"},
+		{header:'${text("生产日期")}', name:'produceDate', index:'a.produce_date', width:110, align:"center"},
+		{header:'${text("订单编号")}', name:'saleOrderNo', index:'a.sale_order_no', width:140, align:"center"},
+		{header:'${text("产线")}', name:'lineTitle', index:'a.line_sn', width:120, align:"center"},
+		{header:'${text("工件类型")}', name:'cateTitle', index:'a.cate_sn', width:120, align:"center"},
+		{header:'${text("状态")}', name:'state', index:'a.state', width:90, align:"center", formatter: function(val, obj, row, act){
+			return workOrderStateMap[val] || val || '';
+		}},
+		{header:'${text("操作")}', name:'actions', width:190, formatter: function(val, obj, row, act){
+			var actions = [];
+			actions.push('<a href="${ctx}/mes/mesWorkOrder/snList?workNo='+(row.workNo||'')+'" class="addTabPage" data-title="${text("工单工件")}" title="${text("工件")}"><i class="fa fa-list"></i></a>&nbsp;');
+			<% if(hasPermi('mes:mesWorkOrder:edit')){ %>
+				actions.push('<a href="${ctx}/mes/mesWorkOrder/form?id='+row.id+'" class="btnList" title="${text("编辑")}"><i class="fa fa-pencil"></i></a>&nbsp;');
+				if (row.state == '0'){
+					actions.push('<a href="${ctx}/mes/mesWorkOrder/issue?id='+row.id+'" class="btnList" title="${text("下达")}" data-confirm="${text("确认下达该工单吗?")}"><i class="glyphicon glyphicon-ok"></i></a>&nbsp;');
+					actions.push('<a href="${ctx}/mes/mesWorkOrder/delete?id='+row.id+'" class="btnList" title="${text("删除")}" data-confirm="${text("确认删除该工单吗?")}"><i class="fa fa-trash-o"></i></a>&nbsp;');
+				}
+				if (row.state == '1'){
+					actions.push('<a href="${ctx}/mes/mesWorkOrder/recall?id='+row.id+'" class="btnList" title="${text("撤回")}" data-confirm="${text("确认撤回该工单吗?")}"><i class="glyphicon glyphicon-arrow-left"></i></a>&nbsp;');
+				}
+				if (row.state == '0' || row.state == '1'){
+					actions.push('<a href="${ctx}/mes/mesWorkOrder/close?id='+row.id+'" class="btnList" title="${text("关闭")}" data-confirm="${text("确认关闭该工单吗?")}"><i class="glyphicon glyphicon-ban-circle"></i></a>&nbsp;');
+				}
+			<% } %>
+			return actions.join('');
+		}}
+	],
+	ajaxSuccess: function(data){
+	}
+});
+</script>
+<script>
+$('#btnExport').click(function(){
+	js.ajaxSubmitForm($('#searchForm'), {
+		url:'${ctx}/mes/mesWorkOrder/exportData',
+		downloadFile:true
+	});
+});
+$('#btnImport').click(function(){
+	js.layer.open({
+		type: 1,
+		area: ['420px'],
+		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/mesWorkOrder/importData" method="post" enctype="multipart/form-data"
+	class="form-horizontal mt20 mb10" style="overflow:auto;max-height:220px;">
+	<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('可直接导入武汉厂工单排程表。第1行说明、第2行表头、第3行起为数据。')}
+			</div>
+			<div class="mt10 pt5">
+				<a href="${ctx}/mes/mesWorkOrder/importTemplate" class="btn btn-default btn-xs"><i class="fa fa-file-excel-o"></i> ${text('下载模板')}</a>
+			</div>
+		</div>
+	</div>
+</form>
+//--></script>

+ 72 - 0
src/main/resources/views/modules/mes/mesWorkOrderSnList.html

@@ -0,0 +1,72 @@
+<% 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>
+			</div>
+		</div>
+		<div class="box-body">
+			<#form:form id="searchForm" model="${mesWorkOrderSn}" action="${ctx}/mes/mesWorkOrder/snListData" 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="workNo" maxlength="64" 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="100" class="form-control width-260"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('工件状态')}:</label>
+					<div class="control-inline width-120">
+						<#form:select path="productState" dictType="mes_product_status" blankOption="true" class="form-control"/>
+					</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>
+var stationResultMap = {'OK':'合格','NG':'不合格'};
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("工单号")}', name:'workNo', width:150, align:"center", frozen:true},
+		{header:'${text("工件码")}', name:'sn', width:280, align:"center", frozen:true, formatter: function(val, obj, row, act){
+			return '<a href="${ctx}/mes/mesProductRecord/list?sn='+(val||'')+'" class="addTabPage" data-title="${text("生产记录")}">'+(val||'')+'</a>';
+		}},
+		{header:'${text("工件类型")}', name:'cateTitle', width:120, align:"center"},
+		{header:'${text("产线")}', name:'lineTitle', width:140, align:"center"},
+		{header:'${text("上线时间")}', name:'onlineDate', width:160, align:"center"},
+		{header:'${text("工件状态")}', name:'productState', width:100, align:"center", formatter: function(val, obj, row, act){
+			return js.getDictLabel(${@DictUtils.getDictListJson('mes_product_status')}, val, '${text("未知")}', true);
+		}},
+		{header:'${text("最近工位")}', name:'oprnoTitle', width:180, align:"center", formatter: function(val, obj, row, act){
+			if (!row.currentOprno){
+				return '${text("未加工")}';
+			}
+			return (val || '') + (row.currentOprno ? '('+row.currentOprno+')' : '');
+		}},
+		{header:'${text("工位结果")}', name:'stationResult', width:90, align:"center", formatter: function(val, obj, row, act){
+			return stationResultMap[val] || val || '';
+		}}
+	],
+	ajaxSuccess: function(data){
+	}
+});
+</script>