Pārlūkot izejas kodu

工序计件模块

wangxichen 5 dienas atpakaļ
vecāks
revīzija
2e0db738c1

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

@@ -3587,4 +3587,181 @@ public class MesProductRecordController extends BaseController {
 		return resp;
 	}
 
+	/**
+	 * 工序计件统计页面
+	 */
+	@RequiresPermissions("mes:mesProductRecord:view")
+	@RequestMapping(value = "piecework")
+	public String piecework(Model model, HttpServletRequest request) {
+		// 班次列表
+		MesShift shiftQuery = new MesShift();
+		shiftQuery.setStatus("0");
+		List<MesShift> shiftList = mesShiftService.findList(shiftQuery);
+		model.addAttribute("shiftList", shiftList);
+
+		// 当前班次id(用于页面默认选中)
+		String curShiftId = "";
+		com.jeesite.modules.mes.resp.MesShiftDataResp curShiftResp = mesShiftService.getCurShift();
+		if (curShiftResp != null && curShiftResp.getMesShift() != null) {
+			curShiftId = curShiftResp.getMesShift().getId();
+		}
+		model.addAttribute("curShiftId", curShiftId);
+
+		// 产品前缀列表
+		String prefixConf = Global.getConfig("mes.piecework.product.prefixes");
+		model.addAttribute("productPrefixes", prefixConf);
+		List<String> prefixList = new ArrayList<>();
+		if (!StringUtils.isBlank(prefixConf)) {
+			for (String p : prefixConf.split(",")) {
+				String t = p.trim();
+				if (!t.isEmpty()) prefixList.add(t);
+			}
+		}
+		model.addAttribute("prefixList", prefixList);
+		return "modules/mes/mesPieceworkList";
+	}
+
+	/**
+	 * 工序计件统计数据接口
+	 */
+	@RequiresPermissions("mes:mesProductRecord:view")
+	@RequestMapping(value = "pieceworkData")
+	@ResponseBody
+	public Object pieceworkData(HttpServletRequest request) {
+		String dateStr   = request.getParameter("date");     // yyyy-MM-dd
+		String shiftId   = request.getParameter("shiftId");  // 班次id,空=全天
+		String oprnoFilter = request.getParameter("oprno");  // 工位号筛选,可空
+
+		// 默认今天
+		if (StringUtils.isBlank(dateStr)) {
+			dateStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
+		}
+
+		// 读产品前缀配置
+		String prefixConf = Global.getConfig("mes.piecework.product.prefixes");
+		List<String> prefixes = new ArrayList<>();
+		if (!StringUtils.isBlank(prefixConf)) {
+			for (String p : prefixConf.split(",")) {
+				String t = p.trim();
+				if (!t.isEmpty()) prefixes.add(t);
+			}
+		}
+
+		// 计算时间范围
+		String dateStart;
+		String dateEnd;
+		try {
+			if (!StringUtils.isBlank(shiftId)) {
+				// 按班次时间段
+				MesShift shiftQuery = new MesShift();
+				shiftQuery.setId(shiftId);
+				MesShift shift = mesShiftService.get(shiftQuery);
+				if (shift == null) {
+					return renderResult(Global.FALSE, text("班次不存在"));
+				}
+				// 跨天班次:结束时间属于次日
+				if (shift.getNext() != null && shift.getNext() == 1) {
+					String startTime = shift.getStart();
+					String endTime   = shift.getEnd();
+					dateStart = dateStr + " " + startTime + ":00";
+					dateEnd   = getNextDay(dateStr) + " " + endTime + ":00";
+				} else {
+					dateStart = dateStr + " " + shift.getStart() + ":00";
+					dateEnd   = dateStr + " " + shift.getEnd()   + ":00";
+				}
+			} else {
+				// 全天
+				dateStart = dateStr + " 00:00:00";
+				dateEnd   = dateStr + " 23:59:59";
+			}
+		} catch (Exception e) {
+			return renderResult(Global.FALSE, text("时间解析失败:" + e.getMessage()));
+		}
+
+		Map<String, Object> params = new HashMap<>();
+		params.put("dateStart", dateStart);
+		params.put("dateEnd",   dateEnd);
+		params.put("prefixes",  prefixes);
+
+		List<Map<String, Object>> rawList = mesProductRecordService.pieceworkStat(params);
+
+		// 读 XT 产线所有工序
+		MesLineProcess lpQuery = new MesLineProcess();
+		lpQuery.setLineSn("XT");
+		lpQuery.setStatus("0");
+		List<MesLineProcess> allProcess = mesLineProcessService.findListOrder(lpQuery);
+
+		// 统计数据 map:oprno -> {prefix -> cnt}
+		Map<String, Map<String, Integer>> cntMap = new HashMap<>();
+		for (Map<String, Object> r : rawList) {
+			String oprno  = (String) r.get("oprno");
+			String prefix = (String) r.get("productPrefix");
+			int    cnt    = ((Number) r.get("cnt")).intValue();
+			cntMap.computeIfAbsent(oprno, k -> new HashMap<>())
+			      .merge(prefix, cnt, Integer::sum);
+		}
+
+		// 按父级分组
+		Map<String, MesLineProcess> idMap    = new LinkedHashMap<>();
+		Map<String, List<MesLineProcess>> childMap = new LinkedHashMap<>();
+		List<MesLineProcess> rootList = new ArrayList<>();
+
+		for (MesLineProcess lp : allProcess) {
+			idMap.put(lp.getId(), lp);
+		}
+		for (MesLineProcess lp : allProcess) {
+			String pid = lp.getPid();
+			if (StringUtils.isBlank(pid) || "0".equals(pid)) {
+				rootList.add(lp);
+			} else {
+				childMap.computeIfAbsent(pid, k -> new ArrayList<>()).add(lp);
+			}
+		}
+
+		// 组装分组结果
+		List<Map<String, Object>> groups = new ArrayList<>();
+		for (MesLineProcess root : rootList) {
+			List<MesLineProcess> children = childMap.getOrDefault(root.getId(), new ArrayList<>());
+			List<Map<String, Object>> rows = new ArrayList<>();
+
+			Map<String, Object> parentRow = new LinkedHashMap<>();
+			parentRow.put("oprno",     root.getOprno());
+			parentRow.put("prefixMap", cntMap.getOrDefault(root.getOprno(), new HashMap<>()));
+			rows.add(parentRow);
+
+			for (MesLineProcess child : children) {
+				Map<String, Object> childRow = new LinkedHashMap<>();
+				childRow.put("oprno",     child.getOprno());
+				childRow.put("prefixMap", cntMap.getOrDefault(child.getOprno(), new HashMap<>()));
+				rows.add(childRow);
+			}
+
+			Map<String, Object> group = new LinkedHashMap<>();
+			group.put("title", root.getTitle());
+			group.put("rows",  rows);
+			groups.add(group);
+		}
+
+		Map<String, Object> resp = new HashMap<>();
+		resp.put("result",   1);
+		resp.put("groups",   groups);
+		resp.put("prefixes", prefixes);
+		resp.put("date",     dateStr);
+		return resp;
+	}
+
+	/** 获取次日日期字符串 */
+	private String getNextDay(String dateStr) {
+		try {
+			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+			Date d = sdf.parse(dateStr);
+			Calendar cal = Calendar.getInstance();
+			cal.setTime(d);
+			cal.add(Calendar.DAY_OF_MONTH, 1);
+			return sdf.format(cal.getTime());
+		} catch (Exception e) {
+			return dateStr;
+		}
+	}
+
 }

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

@@ -55,8 +55,8 @@ jdbc:
   type: mysql
   driver: com.mysql.cj.jdbc.Driver
 
-  url: jdbc:mysql://127.0.0.1:3306/mes_cloud_610?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
-#  url: jdbc:mysql://192.168.113.99:3306/mes_cloud_610?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
+#  url: jdbc:mysql://127.0.0.1:3306/mes_cloud_610?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
+  url: jdbc:mysql://192.168.113.99:3306/mes_cloud_610?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
 #  username: mes_cloud_610
 #  password: wrBrhmpBDxkATCLa
 #  username: root
@@ -999,5 +999,16 @@ msg:
 # 计件统计产品前缀配置
 mes:
   piecework:
+    # 本机统计的产品前缀(多服务器汇总后大屏展示的列顺序)
     product:
-      prefixes: +KA64,+KB23
+      prefixes: +KB24,+KA99,+KB23,+KA64
+    # 多服务器数据汇总
+    aggregate:
+      enable: 1
+      # 其他大屏服务器,逗号分隔,不带尾部斜杠
+      peers: http://192.168.110.99:8980,http://192.168.113.99:8980
+      # 单peer连接/读取超时(毫秒)
+      timeoutMs: 2000
+      # 部分老服务返回 row.data 而无 prefixMap 时,按此规则把 data 注入到指定前缀
+      # 格式:peer1=prefix1;peer2=prefix2
+      peerPrefixHints: http://192.168.112.99:8980=+KB23

+ 329 - 0
src/main/resources/views/modules/mes/mesPieceworkList.html

@@ -0,0 +1,329 @@
+<% layout('/layouts/default.html', {title: '工序计件统计', libs: ['dataGrid']}){ %>
+
+<style>
+/* 顶部操作栏 */
+.pw-header {
+    display: flex; align-items: center; justify-content: space-between;
+    background: #fff; border: 1px solid #e8e8e8; border-radius: 6px;
+    padding: 12px 20px; margin-bottom: 14px;
+}
+.pw-header-left { display: flex; align-items: center; gap: 20px; }
+.pw-date-label  { font-size: 15px; color: #333; font-weight: 600; }
+.pw-shift-cur   { font-size: 13px; color: #888; }
+.pw-shift-cur span { color: #1890ff; font-weight: 600; }
+
+/* 班次 Tab */
+.pw-tabs { display: flex; gap: 6px; align-items: stretch; }
+.pw-tab  {
+    min-width: 72px; padding: 0 16px; border-radius: 4px; border: 1px solid #d9d9d9;
+    background: #fafafa; color: #555; cursor: pointer; font-size: 13px;
+    text-align: center; transition: all .15s; user-select: none;
+    display: flex; flex-direction: column; align-items: center; justify-content: center;
+    min-height: 48px;
+}
+.pw-tab:hover  { border-color: #1890ff; color: #1890ff; }
+.pw-tab.active { background: #1890ff; border-color: #1890ff; color: #fff; font-weight: 600; }
+
+/* 汇总卡片 */
+.pw-cards { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
+.pw-card  {
+    flex: 1; min-width: 130px; background: #fff; border-radius: 8px;
+    border: 1px solid #e8e8e8; padding: 16px 20px 12px; text-align: center;
+    box-shadow: 0 1px 4px rgba(0,0,0,.05);
+}
+.pw-card-label { font-size: 12px; color: #999; margin-bottom: 8px; }
+.pw-card-num   { font-size: 40px; font-weight: 700; line-height: 1; color: #1890ff; }
+.pw-card.c-green  .pw-card-num { color: #27ae60; }
+.pw-card.c-orange .pw-card-num { color: #e67e22; }
+
+/* 主表格容器:左右两列并排 */
+.pw-table-wrap {
+    display: flex; gap: 0; background: #fff;
+    border: 1px solid #d0d7de; border-radius: 6px; overflow: hidden;
+}
+/* 左右两个产品区域各占一半 */
+.pw-col { flex: 1; min-width: 0; overflow-x: auto; }
+.pw-col + .pw-col { border-left: 1px solid #d0d7de; }
+
+/* 表格本身 */
+.pw-tbl { width: 100%; border-collapse: collapse; table-layout: fixed; }
+
+/* 产品大标题行 */
+.pw-tbl .th-product {
+    background: #e6f4ff; color: #1890ff; font-size: 14px; font-weight: 700;
+    text-align: center; padding: 10px 0; border-bottom: 1px solid #d0d7de;
+    letter-spacing: 1px;
+}
+.pw-col:nth-child(2) .th-product { background: #fff7e6; color: #e67e22; border-bottom-color: #d0d7de; }
+
+/* 列头 */
+.pw-tbl .th-col {
+    background: #f6f8fa; font-size: 12px; color: #57606a; font-weight: 600;
+    padding: 8px 10px; border-bottom: 1px solid #d0d7de;
+    text-align: center; white-space: nowrap;
+}
+
+/* 数据行 */
+.pw-tbl td {
+    padding: 0 10px; border-bottom: 1px solid #eaeef2;
+    font-size: 13px; vertical-align: middle;
+}
+.pw-tbl tr:last-child td { border-bottom: none; }
+
+/* 工序组交替背景色 */
+.pw-tbl tr.group-odd  td { background: #fff; }
+.pw-tbl tr.group-even td { background: #f6f8ff; }
+
+/* hover 高亮 */
+.pw-tbl tr.row-hover td { background: #dbeafe !important; }
+
+/* 工位名称列 */
+.td-name {
+    text-align: center; font-weight: 600; color: #24292f; font-size: 11px;
+    border-right: 1px solid #eaeef2; width: 110px; line-height: 1.4;
+    vertical-align: middle; padding: 0 6px;
+    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+}
+/* 工位号列 */
+.td-oprno { text-align: center; color: #888; font-size: 12px; width: 72px;
+            border-right: 1px solid #eaeef2; height: 38px; line-height: 38px; }
+/* 数量列 */
+.td-cnt   { text-align: center; font-size: 16px; font-weight: 700; color: #27ae60; }
+/* 小计行 */
+.td-subtotal-label { text-align: right; font-size: 11px; color: #888;
+                     border-right: 1px solid #eaeef2; padding-right: 8px; }
+.td-subtotal-num   { text-align: center; font-size: 14px; font-weight: 700; color: #1890ff; }
+.tr-subtotal td    { background: #f0f7ff !important; border-top: 1px solid #d0d7de; }
+
+/* 空/loading */
+#loadingTip { padding: 50px 0; text-align: center; color: #aaa; font-size: 14px; }
+#emptyTip   { padding: 60px 0; text-align: center; color: #bbb; font-size: 15px;
+              background: #fff; border-radius: 6px; border: 1px solid #eaeef2; }
+#emptyTip i { font-size: 40px; display: block; margin-bottom: 12px; }
+</style>
+
+<div class="main-content">
+    <div class="box box-main">
+        <div class="box-header">
+            <div class="box-title"><i class="fa fa-bar-chart"></i> 工序计件统计</div>
+        </div>
+        <div class="box-body">
+
+            <!-- 顶部操作栏 -->
+            <div class="pw-header">
+                <div class="pw-header-left">
+                    <div>
+                        <input type="text" id="queryDate" class="form-control laydate"
+                               data-type="date" data-format="yyyy-MM-dd"
+                               style="display:inline-block;width:130px;cursor:pointer;"/>
+                    </div>
+                    <div class="pw-shift-cur">当前班次:<span id="curShiftLabel">-</span></div>
+                </div>
+                <div class="pw-tabs" id="shiftTabs">
+                    <div class="pw-tab" data-shift-id="" data-shift-name="全天">全天</div>
+                    <% for(var s in shiftList){ %>
+                    <div class="pw-tab" data-shift-id="${s.id}" data-shift-name="${s.title}">
+                        ${s.title}<br/><small style="font-size:11px;opacity:.8;">${s.start}~${s.end}</small>
+                    </div>
+                    <% } %>
+                </div>
+            </div>
+
+            <!-- 汇总卡片 -->
+            <div id="summaryCards" class="pw-cards" style="display:none;"></div>
+
+            <!-- loading -->
+            <div id="loadingTip"><i class="fa fa-spinner fa-spin"></i> 加载中...</div>
+
+            <!-- 主表格(JS渲染) -->
+            <div id="tableWrap" style="display:none;">
+                <div class="pw-table-wrap" id="mainTable"></div>
+            </div>
+
+            <!-- 空状态 -->
+            <div id="emptyTip" style="display:none;">
+                <i class="fa fa-inbox"></i>暂无加工数据
+            </div>
+
+        </div>
+    </div>
+</div>
+
+<script>
+$(function(){
+
+    var curShiftId    = '${curShiftId}';
+    var activeShiftId = curShiftId;
+
+    // 今天日期
+    var now   = new Date();
+    var today = now.getFullYear() + '-'
+              + String(now.getMonth()+1).padStart(2,'0') + '-'
+              + String(now.getDate()).padStart(2,'0');
+    $('#queryDate').val(today);
+
+    if(typeof laydate !== 'undefined'){
+        laydate.render({
+            elem: '#queryDate',
+            type: 'date',
+            format: 'yyyy-MM-dd',
+            done: function(value){
+                doQuery();
+            }
+        });
+    } else {
+        $('#queryDate').on('change', function(){ doQuery(); });
+    }
+    function initTabs(){
+        $('#shiftTabs .pw-tab').each(function(){
+            var sid = String($(this).data('shift-id'));
+            var match = curShiftId ? (sid === String(curShiftId)) : (sid === '');
+            if(match){
+                $(this).addClass('active');
+                $('#curShiftLabel').text($(this).data('shift-name'));
+            }
+        });
+        if($('#shiftTabs .pw-tab.active').length === 0){
+            $('#shiftTabs .pw-tab').first().addClass('active');
+            $('#curShiftLabel').text('全天');
+            activeShiftId = '';
+        }
+    }
+    initTabs();
+
+    // Tab 切换
+    $('#shiftTabs').on('click', '.pw-tab', function(){
+        $('#shiftTabs .pw-tab').removeClass('active');
+        $(this).addClass('active');
+        activeShiftId = String($(this).data('shift-id'));
+        doQuery();
+    });
+
+    doQuery();
+
+    /* ---- 查询 ---- */
+    function doQuery(){
+        var date = $('#queryDate').val() || today;
+        $('#summaryCards').hide();
+        $('#tableWrap').hide();
+        $('#emptyTip').hide();
+        $('#loadingTip').show();
+
+        $.ajax({
+            url: '${ctx}/mes/mesProductRecord/pieceworkData',
+            type: 'GET',
+            data: { date: date, shiftId: activeShiftId, oprno: '' },
+            dataType: 'json',
+            success: function(data){
+                $('#loadingTip').hide();
+                if(data.result != 1){ $('#emptyTip').show(); return; }
+                renderResult(data);
+            },
+            error: function(){ $('#loadingTip').hide(); $('#emptyTip').show(); }
+        });
+    }
+
+    /* ---- 渲染 ---- */
+    function renderResult(data){
+        var groups   = data.groups   || [];
+        var prefixes = data.prefixes || [];
+
+        if(groups.length === 0){ $('#emptyTip').show(); return; }
+
+        /* 汇总卡片 */
+        var totalAll = 0;
+        var prefixTotals = {};
+        prefixes.forEach(function(p){ prefixTotals[p] = 0; });
+        groups.forEach(function(g){
+            (g.rows||[]).forEach(function(row){
+                var pm = row.prefixMap || {};
+                prefixes.forEach(function(p){
+                    var v = pm[p] || 0;
+                    totalAll       += v;
+                    prefixTotals[p] = (prefixTotals[p]||0) + v;
+                });
+            });
+        });
+        var cardColors = ['c-green','c-orange','c-purple',''];
+        var cards = '<div class="pw-card"><div class="pw-card-label">当班合计</div>'
+                  + '<div class="pw-card-num">' + totalAll + '</div></div>';
+        prefixes.forEach(function(p, i){
+            cards += '<div class="pw-card ' + (cardColors[i]||'') + '">'
+                   + '<div class="pw-card-label">' + esc(p) + '</div>'
+                   + '<div class="pw-card-num">' + (prefixTotals[p]||0) + '</div></div>';
+        });
+        $('#summaryCards').html(cards).show();
+
+        /* 左右两列表格,每列对应一个产品前缀 */
+        var html = '';
+        prefixes.forEach(function(prefix, pi){
+            html += '<div class="pw-col">';
+            html += '<table class="pw-tbl">';
+            // 产品大标题
+            html += '<thead><tr><th class="th-product" colspan="4">' + esc(prefix) + '</th></tr>';
+            html += '<tr>'
+                  + '<th class="th-col" style="width:110px;">工位名称</th>'
+                  + '<th class="th-col" style="width:72px;">工位号</th>'
+                  + '<th class="th-col">数量</th>'
+                  + '<th class="th-col" style="width:60px;">小计</th>'
+                  + '</tr></thead>';
+            html += '<tbody>';
+
+            groups.forEach(function(g, gi){
+                var rows = g.rows || [];
+
+                // 过滤:只保留该前缀数量 > 0 的行
+                var visibleRows = rows.filter(function(row){
+                    return ((row.prefixMap||{})[prefix] || 0) > 0;
+                });
+
+                // 整组都没数据则跳过
+                if(visibleRows.length === 0) return;
+
+                var rowCnt = visibleRows.length;
+                var groupClass = (gi % 2 === 0) ? 'group-odd' : 'group-even';
+                // 计算该工序组在此前缀下的小计
+                var groupTotal = visibleRows.reduce(function(sum, row){
+                    return sum + (((row.prefixMap||{})[prefix]) || 0);
+                }, 0);
+
+                visibleRows.forEach(function(row, ri){
+                    html += '<tr class="' + groupClass + '" data-group="' + prefix + '-' + gi + '">';
+                    if(ri === 0){
+                        html += '<td class="td-name" rowspan="' + rowCnt + '">' + esc(g.title||'') + '</td>';
+                    }
+                    html += '<td class="td-oprno">' + esc(row.oprno||'') + '</td>';
+                    var cnt = (row.prefixMap||{})[prefix] || 0;
+                    html += '<td class="td-cnt">' + cnt + '</td>';
+                    // 小计列:只在第一行输出,rowspan 跨所有子行
+                    if(ri === 0){
+                        html += '<td class="td-subtotal-num" rowspan="' + rowCnt + '">' + groupTotal + '</td>';
+                    }
+                    html += '</tr>';
+                });
+            });
+
+            html += '</tbody></table></div>';
+        });
+
+        $('#mainTable').html(html);
+
+        // 绑定整组 hover:同一 data-group 的所有行一起高亮
+        $('#mainTable').on('mouseenter', 'tr[data-group]', function(){
+            var g = $(this).data('group');
+            $('tr[data-group="' + g + '"]').addClass('row-hover');
+        }).on('mouseleave', 'tr[data-group]', function(){
+            var g = $(this).data('group');
+            $('tr[data-group="' + g + '"]').removeClass('row-hover');
+        });
+
+        $('#tableWrap').show();
+    }
+
+    function esc(s){
+        return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+    }
+});
+</script>
+
+<% } %>