From c425a8afba0a76eb62d5650cc9c98c42d8339f06 Mon Sep 17 00:00:00 2001
From: zhuguifei <312353457@qq.com>
Date: 星期四, 12 三月 2026 13:01:23 +0800
Subject: [PATCH] perf: 1.优化储丝柜单柜卷包产量计算方式,支持未出料结束计算统计   2.新增储丝柜单柜卷包产量桑基图

---
 ruoyi-plus-soybean/src/router/elegant/routes.ts                                                                         |    9 +
 ruoyi-plus-soybean/src/views/analy/store-silk/index.vue                                                                 |  137 ++++++++++++++--
 ruoyi-plus-soybean/src/hooks/common/echarts.ts                                                                          |   14 +
 RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/StoreSilkInfoServiceImpl.java     |  102 ++++++++++++
 ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-search.vue                                             |  111 +++++++++----
 RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkDetailVo.java               |   12 +
 ruoyi-plus-soybean/src/views/analy/feed-match/modules/feed-match-search.vue                                             |   38 +++
 ruoyi-plus-soybean/src/typings/api/analy.feed-match.api.d.ts                                                            |    2 
 ruoyi-plus-soybean/src/router/elegant/imports.ts                                                                        |    1 
 RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkInfoVo.java                 |   12 +
 ruoyi-plus-soybean/src/typings/elegant-router.d.ts                                                                      |    2 
 RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/FeedmatchTimeDataServiceImpl.java |    5 
 ruoyi-plus-soybean/src/views/analy/feed-match/index.vue                                                                 |    1 
 RuoYi-Vue-Plus/开发日志.md                                                                                                  |    2 
 ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-detail.vue                                             |   25 +-
 ruoyi-plus-soybean/src/router/elegant/transform.ts                                                                      |    1 
 16 files changed, 393 insertions(+), 81 deletions(-)

diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkDetailVo.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkDetailVo.java
index f08f5b8..d312ac1 100644
--- a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkDetailVo.java
+++ b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkDetailVo.java
@@ -49,7 +49,17 @@
     private Date shiftEndTime;
 
     /**
-     * 璇ョ彮娆″唴璁$畻鍑虹殑浜ч噺
+     * 璇ョ彮娆″唴璁$畻鍑虹殑浜ч噺锛堜綔涓鸿褰曟椂姝f暟琛ㄧず澧炲姞锛岃礋鏁拌〃绀哄墧闄わ級
      */
     private Double output;
+
+    /**
+     * 鎿嶄綔绫诲瀷鎻忚堪锛堝 "鐝绱", "鎵i櫎澶撮儴", "鎵i櫎灏鹃儴"锛�
+     */
+    private String calcType;
+
+    /**
+     * 鍛戒腑鏁版嵁鐨勬椂闂寸偣锛堢敤浜庤拷婧暟鎹潵婧愶級
+     */
+    private Date hitTime;
 }
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkInfoVo.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkInfoVo.java
index e7d9e7e..fcac65e 100644
--- a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkInfoVo.java
+++ b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkInfoVo.java
@@ -106,7 +106,17 @@
     private List<StoreSilkDetailVo> rollerDetailList;
 
     /**
-     * 鍖呰鏈轰骇閲忔槑缁�
+     * 鍖呰鏈轰骇閲忔槑缁嗭紙鏈�缁堢粨鏋滐級
      */
     private List<StoreSilkDetailVo> packerDetailList;
+
+    /**
+     * 鍗锋帴鏈烘搷浣滆繃绋嬭褰曪紙澧炲噺鏄庣粏锛�
+     */
+    private List<StoreSilkDetailVo> rollerRecordList;
+
+    /**
+     * 鍖呰鏈烘搷浣滆繃绋嬭褰曪紙澧炲噺鏄庣粏锛�
+     */
+    private List<StoreSilkDetailVo> packerRecordList;
 }
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/FeedmatchTimeDataServiceImpl.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/FeedmatchTimeDataServiceImpl.java
index 4095578..b9c1689 100644
--- a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/FeedmatchTimeDataServiceImpl.java
+++ b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/FeedmatchTimeDataServiceImpl.java
@@ -9,6 +9,7 @@
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.dromara.qa.analy.domain.StoreSilkInfo;
 import org.springframework.stereotype.Service;
 import org.dromara.qa.analy.domain.bo.FeedmatchTimeDataBo;
 import org.dromara.qa.analy.domain.vo.FeedmatchTimeDataVo;
@@ -74,7 +75,9 @@
     private LambdaQueryWrapper<FeedmatchTimeData> buildQueryWrapper(FeedmatchTimeDataBo bo) {
         Map<String, Object> params = bo.getParams();
         LambdaQueryWrapper<FeedmatchTimeData> lqw = Wrappers.lambdaQuery();
-        lqw.eq(bo.getTime() != null, FeedmatchTimeData::getTime, bo.getTime());
+        lqw.eq(bo.getShift() != null, FeedmatchTimeData::getShift, bo.getShift());
+        lqw.between(params.get("beginTime") != null && params.get("endTime") != null,
+                FeedmatchTimeData::getTime, params.get("beginTime"), params.get("endTime"));
         lqw.eq(StringUtils.isNotBlank(bo.getKey()), FeedmatchTimeData::getKey, bo.getKey());
         return lqw;
     }
diff --git a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/StoreSilkInfoServiceImpl.java b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/StoreSilkInfoServiceImpl.java
index eaf9a1c..03254ad 100644
--- a/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/StoreSilkInfoServiceImpl.java
+++ b/RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/StoreSilkInfoServiceImpl.java
@@ -219,9 +219,12 @@
             // 鍗峰寘浜ч噺缁熻
             Double rollerOutput = 0.0;
             Double packerOutput = 0.0;
-            // 鏄庣粏鍒楄〃
+            // 鏄庣粏鍒楄〃锛堜粎瀛樻渶缁堟鏁扮粨鏋滐級
             List<StoreSilkDetailVo> rollerDetailList = new ArrayList<>();
             List<StoreSilkDetailVo> packerDetailList = new ArrayList<>();
+            // 鎿嶄綔璁板綍鍒楄〃锛堝瓨鎵�鏈夊鍑忚繃绋嬶級
+            List<StoreSilkDetailVo> rollerRecordList = new ArrayList<>();
+            List<StoreSilkDetailVo> packerRecordList = new ArrayList<>();
 
             for (int s = 0; s < distShiftList.size(); s++) {
                 MdShiftBo shiftBo = distShiftList.get(s);
@@ -347,6 +350,20 @@
                         // 鍏堟妸缁熻缁撴潫鏃跺埢绱鍊煎姞杩涙潵锛歝urrent = Qty(calcEnd)
                         currentRollerOutput += rData.getQty();
 
+                        // 璁板綍杩囩▼锛氱彮娆℃埅姝㈢疮璁�
+                        StoreSilkDetailVo endRecord = new StoreSilkDetailVo();
+                        endRecord.setFsNum(fsNum.substring(2, 3));
+                        endRecord.setSiloNum(containerNum);
+                        endRecord.setPipeNum(channel);
+                        endRecord.setEquNo(equNo);
+                        endRecord.setShiftCode(shift);
+                        endRecord.setShiftStartTime(calcStartDate);
+                        endRecord.setShiftEndTime(calcEndDate);
+                        endRecord.setOutput(rData.getQty());
+                        endRecord.setCalcType("鐝鎴绱");
+                        endRecord.setHitTime(rData.getTime());
+                        rollerRecordList.add(endRecord);
+
                         // 2) 鎵b�滃ご鈥濓細濡傛灉缁熻寮�濮嬫椂鍒绘櫄浜庣彮娆″紑濮嬶紝鍒欏噺鍘� Qty(calcStart)
                         if (calcStartDate.after(stimDate)) {
                             LocalDateTime calcStartLdt = LocalDateTime.ofInstant(calcStartDate.toInstant(), zone);
@@ -365,6 +382,20 @@
                             RollerTimeData rBeginData = rollerTimeDataMapper.selectOne(beginRlqw);
                             if (rBeginData != null) {
                                 currentRollerOutput -= rBeginData.getQty();
+
+                                // 璁板綍杩囩▼锛氭墸闄ゅご閮ㄤ骇閲�
+                                StoreSilkDetailVo beginRecord = new StoreSilkDetailVo();
+                                beginRecord.setFsNum(fsNum.substring(2, 3));
+                                beginRecord.setSiloNum(containerNum);
+                                beginRecord.setPipeNum(channel);
+                                beginRecord.setEquNo(equNo);
+                                beginRecord.setShiftCode(shift);
+                                beginRecord.setShiftStartTime(calcStartDate);
+                                beginRecord.setShiftEndTime(calcEndDate);
+                                beginRecord.setOutput(-rBeginData.getQty()); // 璐熸暟琛ㄧず鎵i櫎
+                                beginRecord.setCalcType("鎵i櫎鍑烘枡鍓嶇疮璁�");
+                                beginRecord.setHitTime(rBeginData.getTime());
+                                rollerRecordList.add(beginRecord);
                             }
                         }
                     }
@@ -401,6 +432,20 @@
                         // 鍏堟妸缁熻缁撴潫鏃跺埢绱鍊煎姞杩涙潵锛歝urrent = Qty(calcEnd)
                         currentPackerOutput += pData.getTsQty();
 
+                        // 璁板綍杩囩▼锛氱彮娆℃埅姝㈢疮璁�
+                        StoreSilkDetailVo endRecord = new StoreSilkDetailVo();
+                        endRecord.setFsNum(fsNum.substring(2, 3));
+                        endRecord.setSiloNum(containerNum);
+                        endRecord.setPipeNum(channel);
+                        endRecord.setEquNo(equNo);
+                        endRecord.setShiftCode(shift);
+                        endRecord.setShiftStartTime(calcStartDate);
+                        endRecord.setShiftEndTime(calcEndDate);
+                        endRecord.setOutput(pData.getTsQty());
+                        endRecord.setCalcType("鐝鎴绱");
+                        endRecord.setHitTime(pData.getTime());
+                        packerRecordList.add(endRecord);
+
                         // 2) 鎵b�滃ご鈥濓細濡傛灉缁熻寮�濮嬫椂鍒绘櫄浜庣彮娆″紑濮嬶紝鍒欏噺鍘� Qty(calcStart)
                         if (calcStartDate.after(stimDate)) {
                             LocalDateTime calcStartLdt = LocalDateTime.ofInstant(calcStartDate.toInstant(), zone);
@@ -419,6 +464,20 @@
                             PackerTimeData pBeginData = packerTimeDataMapper.selectOne(beginPlqw);
                             if (pBeginData != null) {
                                 currentPackerOutput -= pBeginData.getTsQty();
+
+                                // 璁板綍杩囩▼锛氭墸闄ゅご閮ㄤ骇閲�
+                                StoreSilkDetailVo beginRecord = new StoreSilkDetailVo();
+                                beginRecord.setFsNum(fsNum.substring(2, 3));
+                                beginRecord.setSiloNum(containerNum);
+                                beginRecord.setPipeNum(channel);
+                                beginRecord.setEquNo(equNo);
+                                beginRecord.setShiftCode(shift);
+                                beginRecord.setShiftStartTime(calcStartDate);
+                                beginRecord.setShiftEndTime(calcEndDate);
+                                beginRecord.setOutput(-pBeginData.getTsQty()); // 璐熸暟琛ㄧず鎵i櫎
+                                beginRecord.setCalcType("鎵i櫎鍑烘枡鍓嶇疮璁�");
+                                beginRecord.setHitTime(pBeginData.getTime());
+                                packerRecordList.add(beginRecord);
                             }
                         }
                     }
@@ -444,6 +503,8 @@
             storeSilkInfoVo.setPackerOutput(packerOutput);
             storeSilkInfoVo.setRollerDetailList(rollerDetailList);
             storeSilkInfoVo.setPackerDetailList(packerDetailList);
+            storeSilkInfoVo.setRollerRecordList(rollerRecordList);
+            storeSilkInfoVo.setPackerRecordList(packerRecordList);
 
         }
 
@@ -558,11 +619,44 @@
         LambdaQueryWrapper<StoreSilkInfo> lqw = Wrappers.lambdaQuery();
         lqw.orderByAsc(StoreSilkInfo::getId);
         lqw.like(StringUtils.isNotBlank(bo.getMaterialname()), StoreSilkInfo::getMaterialname, bo.getMaterialname());
-        lqw.eq(StringUtils.isNotBlank(bo.getBatchcode()), StoreSilkInfo::getBatchcode, bo.getBatchcode());
-        lqw.eq(bo.getActualstarttime() != null, StoreSilkInfo::getActualstarttime, bo.getActualstarttime());
+        lqw.like(StringUtils.isNotBlank(bo.getBatchcode()), StoreSilkInfo::getBatchcode, bo.getBatchcode());
+        if (bo.getActualstarttime() != null) {
+            ZoneId zone = ZoneId.systemDefault();
+            LocalDate day = bo.getActualstarttime().toInstant().atZone(zone).toLocalDate();
+            Date dayStart = Date.from(day.atStartOfDay(zone).toInstant());
+            Date nextDayStart = Date.from(day.plusDays(1).atStartOfDay(zone).toInstant());
+            lqw.ge(StoreSilkInfo::getActualstarttime, dayStart);
+            lqw.lt(StoreSilkInfo::getActualstarttime, nextDayStart);
+        }
         lqw.eq(bo.getDistimebegin() != null, StoreSilkInfo::getDistimebegin, bo.getDistimebegin());
         lqw.eq(bo.getDistimeend() != null, StoreSilkInfo::getDistimeend, bo.getDistimeend());
-        lqw.eq(StringUtils.isNotBlank(bo.getSiloid()), StoreSilkInfo::getSiloid, bo.getSiloid());
+        if (StringUtils.isNotBlank(bo.getSiloid())) {
+            // 鏀寔澶氫釜鏌滃彿鏌ヨ锛屼互閫楀彿鍒嗛殧
+            String[] siloids = bo.getSiloid().split(",");
+            lqw.and(wrapper -> {
+                for (String val : siloids) {
+                    val = val.trim();
+                    if (StringUtils.isBlank(val)) {
+                        continue;
+                    }
+                    String finalVal = val;
+                    // 浣跨敤 OR 杩炴帴澶氫釜鏌滃彿鏉′欢
+                    wrapper.or(w -> {
+                        try {
+                            int num = Integer.parseInt(finalVal);
+                            String padded = String.format("%02d", num);
+                            // 鍖归厤 _1 鎴� _01 (鍏煎涓嶈ˉ闆跺拰琛ラ浂鐨勬儏鍐�)
+                            w.likeLeft(StoreSilkInfo::getSiloid, "_" + num)
+                                    .or()
+                                    .likeLeft(StoreSilkInfo::getSiloid, "_" + padded);
+                        } catch (NumberFormatException e) {
+                            // 闈炴暟瀛楀垯鎸夊師鍊煎尮閰�
+                            w.eq(StoreSilkInfo::getSiloid, finalVal);
+                        }
+                    });
+                }
+            });
+        }
         lqw.between(params.get("beginTime") != null && params.get("endTime") != null,
                 StoreSilkInfo::getDistimeend, params.get("beginTime"), params.get("endTime"));
         return lqw;
diff --git "a/RuoYi-Vue-Plus/\345\274\200\345\217\221\346\227\245\345\277\227.md" "b/RuoYi-Vue-Plus/\345\274\200\345\217\221\346\227\245\345\277\227.md"
index 2123e4c..2cd3a9a 100644
--- "a/RuoYi-Vue-Plus/\345\274\200\345\217\221\346\227\245\345\277\227.md"
+++ "b/RuoYi-Vue-Plus/\345\274\200\345\217\221\346\227\245\345\277\227.md"
@@ -13,6 +13,8 @@
 #### 20260302
 - 寰呭姙浜嬮」
 - [ ] 鐩墠鍌ㄤ笣鏌滀俊鎭俊鎭病鏈変粠瑙嗗浘涓幏鍙栵紝浣跨敤oracle_store_silk琛ㄦā鎷�->浣跨敤鍒朵笣杞﹂棿鐪熷疄瑙嗗浘鏁版嵁婧�
+- [ ] 鍒朵笣闆嗘帶锛圤racle 瑙嗗浘锛変腑娌℃湁鍑烘枡缁撴潫鏃堕棿锛屼互鍙婂疄鏃堕噸閲忔槸濡備綍璁$畻
+- [ ] 鍠備笣鏈哄搴斿叧绯伙紝鐩墠redis鏁版嵁搴撳懡鍚嶈鑼�3.4鏈�4鍙锋満锛屽疄闄呰溅闂存病鏈�4鍙锋満锛岄渶鍒扮幇鍦烘牎楠屽搴斿叧绯�
 - [ ] 鍠備笣鏈哄搴斿叧绯伙紝fs寮�澶村偍瀛樼殑鐪熷疄鍊兼槸浠�涔堬紝鐩墠娴嬭瘯鏁版嵁浣跨敤鐨勬槸 fs11 -> 鍌ㄤ笣鏌滃彿
 - [ ] 纭1鍙板杺涓濇満瀵瑰簲鐨�2鍙板偍涓濇煖鏄緷娆′緵涓濊繕鏄彲浠ュ苟琛屼緵涓�(<span style="color: red;">***骞惰渚涗笣鏃犳硶鍑嗙‘璁$畻浜ч噺***</span>)
 - [ ] 纭 鍠備笣鏈哄搴斿叧绯讳腑(key->pipe01	value->1#鏈虹粍瀵瑰簲鐨勫杺涓濇満鍜岀閬�)value涓嶄細閲嶅  (<span style="color: red;">***閲嶅灏嗗鑷磋绠楅�昏緫鍑虹幇閿欒***</span>)
diff --git a/ruoyi-plus-soybean/src/hooks/common/echarts.ts b/ruoyi-plus-soybean/src/hooks/common/echarts.ts
index 8e7c728..d74f93e 100755
--- a/ruoyi-plus-soybean/src/hooks/common/echarts.ts
+++ b/ruoyi-plus-soybean/src/hooks/common/echarts.ts
@@ -1,7 +1,16 @@
 import { computed, effectScope, nextTick, onScopeDispose, shallowRef, watch } from 'vue';
 import { useElementSize } from '@vueuse/core';
 import * as echarts from 'echarts/core';
-import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
+import {
+  BarChart,
+  GaugeChart,
+  LineChart,
+  PictorialBarChart,
+  PieChart,
+  RadarChart,
+  SankeyChart,
+  ScatterChart
+} from 'echarts/charts';
 import type {
   BarSeriesOption,
   GaugeSeriesOption,
@@ -9,6 +18,7 @@
   PictorialBarSeriesOption,
   PieSeriesOption,
   RadarSeriesOption,
+  SankeySeriesOption,
   ScatterSeriesOption
 } from 'echarts/charts';
 import {
@@ -40,6 +50,7 @@
   | PictorialBarSeriesOption
   | RadarSeriesOption
   | GaugeSeriesOption
+  | SankeySeriesOption
   | TitleComponentOption
   | LegendComponentOption
   | TooltipComponentOption
@@ -63,6 +74,7 @@
   PictorialBarChart,
   RadarChart,
   GaugeChart,
+  SankeyChart,
   LabelLayout,
   UniversalTransition,
   CanvasRenderer
diff --git a/ruoyi-plus-soybean/src/router/elegant/imports.ts b/ruoyi-plus-soybean/src/router/elegant/imports.ts
index 4751a41..4d0ffc3 100755
--- a/ruoyi-plus-soybean/src/router/elegant/imports.ts
+++ b/ruoyi-plus-soybean/src/router/elegant/imports.ts
@@ -38,6 +38,7 @@
   monitor_online: () => import("@/views/monitor/online/index.vue"),
   monitor_operlog: () => import("@/views/monitor/operlog/index.vue"),
   qm_batch: () => import("@/views/qm/batch/index.vue"),
+  qm_std: () => import("@/views/qm/std/index.vue"),
   system_client: () => import("@/views/system/client/index.vue"),
   system_config: () => import("@/views/system/config/index.vue"),
   system_dept: () => import("@/views/system/dept/index.vue"),
diff --git a/ruoyi-plus-soybean/src/router/elegant/routes.ts b/ruoyi-plus-soybean/src/router/elegant/routes.ts
index 274f98a..35c99c3 100755
--- a/ruoyi-plus-soybean/src/router/elegant/routes.ts
+++ b/ruoyi-plus-soybean/src/router/elegant/routes.ts
@@ -264,6 +264,15 @@
           title: 'qm_batch',
           i18nKey: 'route.qm_batch'
         }
+      },
+      {
+        name: 'qm_std',
+        path: '/qm/std',
+        component: 'view.qm_std',
+        meta: {
+          title: 'qm_std',
+          i18nKey: 'route.qm_std'
+        }
       }
     ]
   },
diff --git a/ruoyi-plus-soybean/src/router/elegant/transform.ts b/ruoyi-plus-soybean/src/router/elegant/transform.ts
index 7459606..fac85aa 100755
--- a/ruoyi-plus-soybean/src/router/elegant/transform.ts
+++ b/ruoyi-plus-soybean/src/router/elegant/transform.ts
@@ -193,6 +193,7 @@
   "monitor_operlog": "/monitor/operlog",
   "qm": "/qm",
   "qm_batch": "/qm/batch",
+  "qm_std": "/qm/std",
   "social-callback": "/social-callback",
   "system": "/system",
   "system_client": "/system/client",
diff --git a/ruoyi-plus-soybean/src/typings/api/analy.feed-match.api.d.ts b/ruoyi-plus-soybean/src/typings/api/analy.feed-match.api.d.ts
index 2a695e9..eded276 100644
--- a/ruoyi-plus-soybean/src/typings/api/analy.feed-match.api.d.ts
+++ b/ruoyi-plus-soybean/src/typings/api/analy.feed-match.api.d.ts
@@ -70,7 +70,7 @@
 
     /** feed match search params */
     type FeedMatchSearchParams = CommonType.RecordNullable<
-      Pick<Api.Analy.FeedMatch, 'time' | 'key'> & Api.Common.CommonSearchParams
+      Pick<Api.Analy.FeedMatch, 'time' | 'key' | 'shift'> & Api.Common.CommonSearchParams
     >;
 
     /** feed match operate params */
diff --git a/ruoyi-plus-soybean/src/typings/elegant-router.d.ts b/ruoyi-plus-soybean/src/typings/elegant-router.d.ts
index cd2f9bb..013ab97 100755
--- a/ruoyi-plus-soybean/src/typings/elegant-router.d.ts
+++ b/ruoyi-plus-soybean/src/typings/elegant-router.d.ts
@@ -47,6 +47,7 @@
     "monitor_operlog": "/monitor/operlog";
     "qm": "/qm";
     "qm_batch": "/qm/batch";
+    "qm_std": "/qm/std";
     "social-callback": "/social-callback";
     "system": "/system";
     "system_client": "/system/client";
@@ -156,6 +157,7 @@
     | "monitor_online"
     | "monitor_operlog"
     | "qm_batch"
+    | "qm_std"
     | "system_client"
     | "system_config"
     | "system_dept"
diff --git a/ruoyi-plus-soybean/src/views/analy/feed-match/index.vue b/ruoyi-plus-soybean/src/views/analy/feed-match/index.vue
index 0c59e4b..3f303be 100644
--- a/ruoyi-plus-soybean/src/views/analy/feed-match/index.vue
+++ b/ruoyi-plus-soybean/src/views/analy/feed-match/index.vue
@@ -24,6 +24,7 @@
   pageSize: 10,
   time: null,
   key: null,
+  shift: null,
   params: {}
 });
 
diff --git a/ruoyi-plus-soybean/src/views/analy/feed-match/modules/feed-match-search.vue b/ruoyi-plus-soybean/src/views/analy/feed-match/modules/feed-match-search.vue
index 4482f5d..a88e769 100644
--- a/ruoyi-plus-soybean/src/views/analy/feed-match/modules/feed-match-search.vue
+++ b/ruoyi-plus-soybean/src/views/analy/feed-match/modules/feed-match-search.vue
@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { toRaw } from 'vue';
+import { ref, toRaw } from 'vue';
 import { jsonClone } from '@sa/utils';
 import { useNaiveForm } from '@/hooks/common/form';
 import { $t } from '@/locales';
@@ -21,8 +21,30 @@
 const defaultModel = jsonClone(toRaw(model.value));
 
 function resetModel() {
+  timeRange.value = null;
   Object.assign(model.value, defaultModel);
 }
+
+const timeRange = ref<[string, string] | null>(null);
+
+function onTimeRangeUpdate(value: [string, string] | null) {
+  const params = (model.value.params ||= {});
+  const beginTime = value?.[0] ? String(value[0]).trim() : '';
+  const endTime = value?.[1] ? String(value[1]).trim() : '';
+  if (beginTime && endTime) {
+    params.beginTime = beginTime;
+    params.endTime = endTime;
+  } else {
+    delete params.beginTime;
+    delete params.endTime;
+  }
+}
+
+const shiftOptions = [
+  { label: '鏃╃彮', value: 1 },
+  { label: '涓彮', value: 2 },
+  { label: '鏅氱彮', value: 3 }
+];
 
 async function reset() {
   await restoreValidation();
@@ -42,18 +64,20 @@
       <NCollapseItem :title="$t('common.search')" name="analy-feed-match-search">
         <NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
           <NGrid responsive="screen" item-responsive>
-            <NFormItemGi span="24 s:12 m:6" label="鏃堕棿鎴�" label-width="auto" path="time" class="pr-24px">
+            <NFormItemGi span="24 s:12 m:8 l:8 xl:8" label="鏃堕棿鍖洪棿" label-width="auto" path="params.beginTime" class="pr-24px">
               <NDatePicker
-                v-model:formatted-value="model.time"
-                type="datetime"
+                v-model:formatted-value="timeRange"
+                type="datetimerange"
                 value-format="yyyy-MM-dd HH:mm:ss"
                 clearable
+                :default-time="['00:00:00', '23:59:59']"
+                @update:formatted-value="onTimeRangeUpdate"
               />
             </NFormItemGi>
-            <NFormItemGi span="24 s:12 m:6" label="鐝+鏈哄彴" label-width="auto" path="key" class="pr-24px">
-              <NInput v-model:value="model.key" placeholder="璇疯緭鍏ョ彮娆�+鏈哄彴" />
+            <NFormItemGi span="24 s:12 m:8 l:8 xl:8" label="鐝" label-width="auto" path="shift" class="pr-24px">
+              <NSelect v-model:value="model.shift" placeholder="璇烽�夋嫨鐝" :options="shiftOptions" clearable />
             </NFormItemGi>
-            <NFormItemGi :show-feedback="false" span="24" class="pr-24px">
+            <NFormItemGi :show-feedback="false" span="24 s:12 m:8 l:8 xl:8" class="pr-24px">
               <NSpace class="w-full" justify="end">
                 <NButton @click="reset">
                   <template #icon>
diff --git a/ruoyi-plus-soybean/src/views/analy/store-silk/index.vue b/ruoyi-plus-soybean/src/views/analy/store-silk/index.vue
index 89f96b8..141fbeb 100644
--- a/ruoyi-plus-soybean/src/views/analy/store-silk/index.vue
+++ b/ruoyi-plus-soybean/src/views/analy/store-silk/index.vue
@@ -1,5 +1,5 @@
 <script setup lang="tsx">
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
 import { NDivider } from 'naive-ui';
 import { fetchBatchDeleteStoreSilk, fetchGetStoreSilkList } from '@/service/api/analy/store-silk';
 import { useAppStore } from '@/store/modules/app';
@@ -11,6 +11,7 @@
 import StoreSilkOperateDrawer from './modules/store-silk-operate-drawer.vue';
 import StoreSilkSearch from './modules/store-silk-search.vue';
 import StoreSilkDetail from './modules/store-silk-detail.vue';
+import StoreSilkSankey from './modules/store-silk-sankey.vue';
 
 defineOptions({
   name: 'StoreSilkList'
@@ -20,8 +21,32 @@
 const { download } = useDownload();
 const { hasAuth } = useAuth();
 
+
+
 const selectedRollerDetailList = ref<any[]>([]);
 const selectedPackerDetailList = ref<any[]>([]);
+
+// 妗戝熀鍥緎tart
+const showSankey = ref(false);
+const rollerRecordList = ref<any[]>([]);
+const packerRecordList = ref<any[]>([]);
+// 妗戝熀鍥緀nd
+
+const sankeyRollerBoxList = computed(() => {
+  return (rollerRecordList.value || []).map(item => {
+    const v = calcRollerBox((item as any).output);
+    const output = v === null ? null : Number(v.toFixed(2));
+    return { ...(item as any), output };
+  });
+});
+
+const sankeyPackerBoxList = computed(() => {
+  return (packerRecordList.value || []).map(item => {
+    const v = calcPackerBox((item as any).output);
+    const output = v === null ? null : Number(v.toFixed(2));
+    return { ...(item as any), output };
+  });
+});
 
 const searchParams = ref<Api.Analy.StoreSilkSearchParams>({
   pageNum: 1,
@@ -45,9 +70,33 @@
 }
 
 const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } =
-  useNaivePaginatedTable({
+  useNaivePaginatedTable<any, any>({
     api: () => fetchGetStoreSilkList(searchParams.value),
-    transform: response => defaultTransform(response),
+    transform: response => {
+      const respData = (response as any).data;
+      const error = (response as any).error;
+
+      if (!error) {
+        const rows = Array.isArray(respData?.rows) ? respData.rows : [];
+        const apiRollerRecordList = respData?.rollerRecordList;
+        const apiPackerRecordList = respData?.packerRecordList;
+
+        rollerRecordList.value =
+          Array.isArray(apiRollerRecordList) && apiRollerRecordList.length > 0
+            ? apiRollerRecordList
+            : rows.flatMap((r: any) => (Array.isArray(r?.rollerDetailList) ? r.rollerDetailList : []));
+
+        packerRecordList.value =
+          Array.isArray(apiPackerRecordList) && apiPackerRecordList.length > 0
+            ? apiPackerRecordList
+            : rows.flatMap((r: any) => (Array.isArray(r?.packerDetailList) ? r.packerDetailList : []));
+      } else {
+        rollerRecordList.value = [];
+        packerRecordList.value = [];
+      }
+
+      return defaultTransform(response as any);
+    },
     onPaginationParamsChange: params => {
       searchParams.value.pageNum = params.page;
       searchParams.value.pageSize = params.pageSize;
@@ -64,19 +113,19 @@
         key: 'materialname',
         title: '鐗屽彿',
         align: 'center',
-        minWidth: 120
+        minWidth: 96
       },
       {
         key: 'batchcode',
         title: '鎵规鍙�',
         align: 'center',
-        minWidth: 130
+        minWidth: 110
       },
       {
         key: 'rollerOutput',
         title: '鍗锋帴浜ч噺(绠�)',
         align: 'center',
-        minWidth: 120,
+        minWidth: 112,
         render: row => {
           const v = calcRollerBox((row as any).rollerOutput);
           if (v === null) return '-';
@@ -84,10 +133,27 @@
         }
       },
       {
+        key: 'rollerUnitCost',
+        title: '鍗锋帴鍗曡��',
+        align: 'center',
+        minWidth: 96,
+        render: row => {
+          const box = calcRollerBox((row as any).rollerOutput);
+          if (box === null || box <= 0) return '-';
+          const jobinput = Number((row as any).jobinput);
+          const weight = Number((row as any).weight);
+          const diff = jobinput - weight;
+          if (!Number.isFinite(diff)) return '-';
+          const val = diff / box;
+          if (!Number.isFinite(val)) return '-';
+          return val.toFixed(2);
+        }
+      },
+      {
         key: 'packerOutput',
         title: '鍖呰浜ч噺(绠�)',
         align: 'center',
-        minWidth: 120,
+        minWidth: 112,
         render: row => {
           const v = calcPackerBox((row as any).packerOutput);
           if (v === null) return '-';
@@ -95,46 +161,63 @@
         }
       },
       {
+        key: 'packerUnitCost',
+        title: '鍖呰鍗曡��',
+        align: 'center',
+        minWidth: 96,
+        render: row => {
+          const box = calcPackerBox((row as any).packerOutput);
+          if (box === null || box <= 0) return '-';
+          const jobinput = Number((row as any).jobinput);
+          const weight = Number((row as any).weight);
+          const diff = jobinput - weight;
+          if (!Number.isFinite(diff)) return '-';
+          const val = diff / box;
+          if (!Number.isFinite(val)) return '-';
+          return val.toFixed(2);
+        }
+      },
+      {
         key: 'actualstarttime',
         title: '鎶曟枡鏃ユ湡',
         align: 'center',
-        width: 180
+        width: 160
       },
       {
         key: 'jobinput',
         title: '鎶曟枡閲嶉噺',
         align: 'center',
-        minWidth: 120
+        minWidth: 96
       },
       {
         key: 'weight',
         title: '鍌ㄤ笣鏌滈噸閲�',
         align: 'center',
-        minWidth: 120
+        minWidth: 96
       },
       {
         key: 'distimebegin',
         title: '鍌ㄤ笣鏌滃嚭鏂欏紑濮嬫椂闂�',
         align: 'center',
-        width: 180
+        width: 160
       },
       {
         key: 'distimeend',
         title: '鍌ㄤ笣鏌滃嚭鏂欑粨鏉熸椂闂�',
         align: 'center',
-        width: 180
+        width: 160
       },
       {
         key: 'siloid',
         title: '鏌滃瓙鍙�(鏈綅)',
         align: 'center',
-        width: 160
+        width: 120
       }
     ]
   });
 
 const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
-  useTableOperate(data, 'id', getData);
+  useTableOperate<any>(data, 'id', getData);
 
 async function handleBatchDelete() {
   // request
@@ -194,12 +277,22 @@
           :loading="loading"
           :show-add="false"
           :show-delete="false"
-          :show-export="hasAuth('analy:storeSilk:export')"
+          :show-export="!showSankey && hasAuth('analy:storeSilk:export')"
           @add="handleAdd"
           @delete="handleBatchDelete"
           @export="handleExport"
           @refresh="getData"
         >
+          <template #prefix>
+            <NButton
+              size="small"
+              ghost
+              :type="showSankey ? 'primary' : 'default'"
+              @click="showSankey = !showSankey"
+            >
+              {{ showSankey ? '鍒楄〃' : '妗戝熀鍥�' }}
+            </NButton>
+          </template>
           <template #suffix>
             <NPopover placement="bottom-end" trigger="click">
               <template #trigger>
@@ -221,8 +314,16 @@
           </template>
         </TableHeaderOperation>
       </template>
+      <StoreSilkSankey
+        v-if="showSankey"
+        :roller-record-list="sankeyRollerBoxList"
+        :packer-record-list="sankeyPackerBoxList"
+        :rows="data"
+        class="h-full"
+      />
       <NDataTable
-        :columns="columns"
+        v-else
+        :columns="columns as any"
         :data="data"
         :size="tableSize"
         :flex-height="!appStore.isMobile"
@@ -242,6 +343,7 @@
       />
     </NCard>
     <StoreSilkDetail
+      v-if="!showSankey"
       :roller-detail-list="selectedRollerDetailList"
       :packer-detail-list="selectedPackerDetailList"
       class="h-[192px] overflow-hidden"
@@ -252,7 +354,6 @@
 <style scoped>
 :deep(.n-data-table-th),
 :deep(.n-data-table-td) {
-  padding-top: 4px;
-  padding-bottom: 4px;
+  padding: 4px 6px;
 }
 </style>
diff --git a/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-detail.vue b/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-detail.vue
index d652e37..9c0252b 100644
--- a/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-detail.vue
+++ b/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-detail.vue
@@ -55,38 +55,38 @@
     key: 'index',
     title: $t('common.index'),
     align: 'center',
-    width: 64,
+    width: 56,
     render: (_: any, index: number) => index + 1
   },
   {
     key: 'fsNum',
     title: '鍠備笣鏈哄彿',
     align: 'center',
-    width: 100
+    width: 84
   },
   {
     key: 'siloNum',
     title: '鍌ㄤ笣鏌滃彿',
     align: 'center',
-    width: 100
+    width: 84
   },
   {
     key: 'pipeNum',
     title: '绠¢亾鍙�',
     align: 'center',
-    width: 100
+    width: 84
   },
   {
     key: 'equNo',
     title: '鏈哄彴鍙�',
     align: 'center',
-    width: 100
+    width: 72
   },
   {
     key: 'shiftCode',
     title: '鐝浠g爜',
     align: 'center',
-    width: 100,
+    width: 88,
     render: (row: StoreSilkDetailVo) => {
       const map: Record<string, string> = {
         '1': '鏃╃彮',
@@ -100,19 +100,19 @@
     key: 'shiftStartTime',
     title: '鐝寮�濮嬫椂闂�',
     align: 'center',
-    width: 160
+    width: 148
   },
   {
     key: 'shiftEndTime',
     title: '鐝缁撴潫鏃堕棿',
     align: 'center',
-    width: 160
+    width: 148
   },
   {
     key: 'output',
     title: '浜ч噺(鍗冩敮)',
     align: 'center',
-    width: 120,
+    width: 96,
     render: (row: StoreSilkDetailVo) => {
       if (row.output === null || row.output === undefined) return '-';
       const v = Number(row.output);
@@ -124,7 +124,7 @@
     key: 'outputBox',
     title: '浜ч噺(绠�)',
     align: 'center',
-    width: 100,
+    width: 84,
     render: (row: StoreSilkDetailVo) => {
       const v = detailType.value === 'roller' ? calcRollerBox(row.output) : calcPackerBox(row.output);
       if (v === null) return '-';
@@ -170,4 +170,9 @@
 :deep(.n-card__content) {
   padding: 8px 12px;
 }
+
+:deep(.n-data-table-th),
+:deep(.n-data-table-td) {
+  padding: 4px 6px;
+}
 </style>
diff --git a/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-search.vue b/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-search.vue
index ce8a1d2..ffb40b6 100644
--- a/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-search.vue
+++ b/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-search.vue
@@ -34,51 +34,78 @@
 
 const dateRange = ref<[string, string] | null>(null);
 
-onMounted(() => {
-  if (model.value.params?.beginTime && model.value.params?.endTime) {
-    dateRange.value = [model.value.params.beginTime as string, model.value.params.endTime as string];
+function onDateRangeUpdate(value: [string, string] | null) {
+  if (!model.value.params) {
+    model.value.params = {};
+  }
+
+  const beginTime = value?.[0] ? String(value[0]).trim() : '';
+  const endTime = value?.[1] ? String(value[1]).trim() : '';
+
+  if (beginTime && endTime) {
+    model.value.params.beginTime = beginTime;
+    model.value.params.endTime = endTime;
   } else {
-    dateRange.value = getDefaultRange();
-    if (!model.value.params) {
-      model.value.params = {};
-    }
-    model.value.params.beginTime = dateRange.value[0];
-    model.value.params.endTime = dateRange.value[1];
+    delete model.value.params.beginTime;
+    delete model.value.params.endTime;
+  }
+}
+
+function setDefaultRange() {
+  const range = getDefaultRange();
+  dateRange.value = range;
+  onDateRangeUpdate(range);
+}
+
+onMounted(() => {
+  if (!model.value.params) {
+    model.value.params = {};
+  }
+
+  const beginTime = model.value.params.beginTime ? String(model.value.params.beginTime).trim() : '';
+  const endTime = model.value.params.endTime ? String(model.value.params.endTime).trim() : '';
+
+  if (beginTime && endTime) {
+    dateRange.value = [beginTime, endTime];
+  } else {
+    setDefaultRange();
   }
 });
-
-watch(
-  dateRange,
-  val => {
-    if (!model.value.params) {
-      model.value.params = {};
-    }
-    if (val && val[0] && val[1]) {
-      model.value.params.beginTime = val[0];
-      model.value.params.endTime = val[1];
-    } else {
-      model.value.params.beginTime = null;
-      model.value.params.endTime = null;
-    }
-  },
-  { deep: true }
-);
 
 async function reset() {
   await restoreValidation();
   resetModel();
-  dateRange.value = getDefaultRange();
-  if (!model.value.params) {
-    model.value.params = {};
-  }
-  model.value.params.beginTime = dateRange.value[0];
-  model.value.params.endTime = dateRange.value[1];
+  setDefaultRange();
   emit('search');
 }
 
 async function search() {
   await validate();
   emit('search');
+}
+
+const siloOptions = Array.from({ length: 12 }, (_, i) => ({
+  label: `${i + 1}鍙峰偍涓濇煖`,
+  value: String(i + 1)
+}));
+
+const selectedSilos = ref<string[]>([]);
+
+watch(
+  () => model.value.siloid,
+  val => {
+    if (val) {
+      selectedSilos.value = (val as string).split(',');
+    } else {
+      selectedSilos.value = [];
+    }
+  },
+  { immediate: true }
+);
+
+function handleSiloChange(val: string[]) {
+  selectedSilos.value = val;
+  model.value.siloid = val.join(',');
 }
 </script>
 
@@ -95,10 +122,10 @@
               path="materialname"
               class="pr-24px"
             >
-              <NInput v-model:value="model.materialname" placeholder="璇疯緭鍏ョ墝鍙�" />
+              <NInput v-model:value="model.materialname" clearable placeholder="璇疯緭鍏ョ墝鍙�" />
             </NFormItemGi>
             <NFormItemGi span="24 s:12 m:8 l:8 xl:8" label="鎵规鍙�" label-width="auto" path="batchcode" class="pr-24px">
-              <NInput v-model:value="model.batchcode" placeholder="璇疯緭鍏ユ壒娆″彿" />
+              <NInput v-model:value="model.batchcode" clearable placeholder="璇疯緭鍏ユ壒娆″彿" />
             </NFormItemGi>
             <NFormItemGi
               span="24 s:12 m:8 l:8 xl:8"
@@ -109,9 +136,10 @@
             >
               <NDatePicker
                 v-model:formatted-value="model.actualstarttime"
-                type="datetime"
-                value-format="yyyy-MM-dd HH:mm:ss"
+                type="date"
+                value-format="yyyy-MM-dd"
                 clearable
+                class="w-full"
               />
             </NFormItemGi>
             <NFormItemGi
@@ -127,6 +155,8 @@
                 :default-time="['00:00:00', '23:59:59']"
                 value-format="yyyy-MM-dd HH:mm:ss"
                 clearable
+                class="w-full"
+                @update:formatted-value="onDateRangeUpdate"
               />
             </NFormItemGi>
             <NFormItemGi
@@ -136,7 +166,14 @@
               path="siloid"
               class="pr-24px"
             >
-              <NInput v-model:value="model.siloid" placeholder="璇疯緭鍏ユ煖瀛愬彿(鏈綅)" />
+              <NSelect
+                v-model:value="selectedSilos"
+                multiple
+                clearable
+                :options="siloOptions"
+                placeholder="璇烽�夋嫨鏌滃瓙鍙�"
+                @update:value="handleSiloChange"
+              />
             </NFormItemGi>
             <NFormItemGi :show-feedback="false" span="24" class="pr-24px">
               <NSpace class="w-full" justify="end">

--
Gitblit v1.9.3