Selaa lähdekoodia

Merge branch 'dev' of http://110.41.163.243:9980/kd-aiot/kd-aiot-frontend-mes into dev

tanzx 1 kuukausi sitten
vanhempi
commit
e99642885e

+ 14 - 0
src/views/produce/components/feeding/components/batchProductsBom.vue

@@ -214,6 +214,20 @@
       }
     },
 
+    watch: {
+      selection: {
+        handler(newSelection) {
+          this.list.forEach((item) => {
+            if (newSelection.includes(item)) {
+              this.$set(item, 'confirm', 1);
+            } else {
+              this.$set(item, 'confirm', 0);
+            }
+          });
+        }
+      }
+    },
+
     computed: {
       columns() {
         let action = {};

+ 2 - 2
src/views/produce/components/feeding/details.vue

@@ -33,7 +33,7 @@
           </div>
         </div>
 
-        <div v-for="(it, idx) in item.orderInfoList" :key="idx">
+        <div v-for="(it, idx) in item.orderInfoList" :key="it.taskId">
           <workOrderBom
             :item="it"
             :isDetails="true"
@@ -235,7 +235,7 @@
           taskId: taskId
         };
         const res = await feedByOrderIds(param);
-        console.log(res);
+        console.log('feedByOrderIds',res);
         this.dataList = JSON.parse(JSON.stringify(res));
         console.log(this.dataList);
       }

+ 29 - 11
src/views/produce/components/feeding/index.vue

@@ -996,28 +996,46 @@
         if (!isCache) {
           return false;
         }
-        this.loading = this.$loading({
-          lock: true,
-          text: '加载中',
-          background: 'rgba(0, 0, 0, 0.7)'
+
+        // 不修改 this.List,新建一个 filterList,内部 product 只保留 confirm === 1
+        let filterList = this.List.map((item) => {
+          return {
+            ...item,
+            product: Array.isArray(item.product)
+              ? item.product.filter((p) => p.confirm === 1)
+              : item.product
+          };
+        }).filter((item) => {
+          return Array.isArray(item.product) && item.product.length > 0;
         });
 
+        if(!filterList.length) filterList = this.List;
         let newList = [];
-        for (let item of this.List) {
+
+        for (let item of filterList) {
           newList.push({
             currentTaskDiagram: item.currentTaskDiagram,
             workOrderId: item.workOrderId
           });
         }
 
+        console.log("newList",newList);
+
+        this.loading = this.$loading({
+          lock: true,
+          text: '加载中',
+          background: 'rgba(0, 0, 0, 0.7)'
+        });
+       
+
         await checkRepeatFeed(newList).then((res) => {
           if (res.data.length != 0) {
-            this.checkFeed(res.data);
+            this.checkFeed(res.data, filterList);
           }
         });
       },
 
-      checkFeed(list) {
+      checkFeed(list, filterList) {
         let wordCodeList = [];
         for (let item of list) {
           if (item.feedFormed != '0') {
@@ -1037,18 +1055,18 @@
             }
           )
             .then(() => {
-              this.feedReport();
+              this.feedReport(filterList);
             })
             .catch(() => {
               this.loading.close();
             });
         } else {
-          this.feedReport();
+          this.feedReport(filterList);
         }
       },
 
-      feedReport() {
-        batchSave(this.List)
+      feedReport(filterList) {
+        batchSave(filterList)
           .then((res) => {
             this.loading.close();
             this.$message.success('投料成功');

+ 1 - 1
src/views/produce/components/jobBooking/components/jobBom.vue

@@ -281,7 +281,7 @@
             const result = finalList.reduce(
               (acc, item) => {
                 const qty = getQty(item);
-
+                if(isDetails) return acc;
                 if (item.extInfo.isLoss == 1) {
                   // 损耗计入损耗数量
                   acc.lossNum = this.add(acc.lossNum, qty);

+ 310 - 13
src/views/produce/components/routings.vue

@@ -14,19 +14,64 @@
         工艺路线: <span>{{ routeObj.produceRoutingName }}</span></div
       >
       <div>
-        <el-steps :active="activeIndex" space="20px" align-center>
-          <el-step
-            v-for="(item, index) in routeList"
-            :key="index"
-            :title="item.taskTypeName"
-            @click.native="handIdx(index, item)"
+        <div
+          class="route-flow"
+          :style="{ height: stepsRowHeight + 'px' }"
+        >
+          <div
+            v-for="(col, colIndex) in stepColumns"
+            :key="`route-unit-${colIndex}`"
+            class="route-flow-unit"
           >
-            <template v-slot:description>
-              <!-- <div>投:{{  item.feedExistNum || 0  }} 报:{{item.reportExistNum  || 0}}</div> -->
-              <div>{{ desIndex == index ? '此处' : '' }}</div>
-            </template>
-          </el-step>
-        </el-steps>
+            <div v-if="colIndex > 0" class="route-connector">
+              <svg
+                class="route-connector-svg"
+                :viewBox="`0 0 ${connectorViewW} ${stepsRowHeight}`"
+                preserveAspectRatio="none"
+              >
+                <path
+                  v-for="(line, li) in connectorLines(
+                    stepColumns[colIndex - 1],
+                    col
+                  )"
+                  :key="li"
+                  :d="line.d"
+                  class="route-connector-line"
+                />
+              </svg>
+            </div>
+            <div class="route-col">
+              <div
+                v-for="row in col.items"
+                :key="row.flatIndex"
+                class="route-node-row"
+                :class="{ 'is-parallel': row.item.isParallel == 1 }"
+                :style="{ height: stepNodeH + 'px' }"
+              >
+                <div
+                  class="route-node"
+                  :class="{
+                    'is-current': row.flatIndex === desIndex,
+                    'is-error': row.flatIndex === errorIndex,
+                    'is-done': row.flatIndex < activeIndex,
+                    'is-compact': row.item.isParallel == 1
+                  }"
+                  :title="row.item.taskTypeName"
+                  @click="handIdx(row.flatIndex, row.item)"
+                >
+                  <div class="route-node-name">
+                    {{ row.item.taskTypeName
+                    }}<span
+                      v-if="row.flatIndex === desIndex"
+                      class="route-node-tag"
+                      >此处</span
+                    >
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
         <el-tabs type="border-card">
           <el-tab-pane label="领料记录">
             <!-- <feedDetails
@@ -99,12 +144,88 @@
         routeList: [],
         activeIndex: 0,
         desIndex: 0,
+        errorIndex: 10000,
         newId: '',
         curTaskObj: null,
-        sourceTaskId: ''
+        sourceTaskId: '',
+        stepNodeH: 38, // 每个工序节点的行高
+        stepConnectorW: 12, // 列间连接线 SVG 的最小宽度
+        connectorViewW: 56 // 连接线 SVG 的实际宽度(自适应测量)
       };
     },
+    computed: {
+      // 按 isParallel 连续段把工序拆分成"列",并行工序同列垂直堆叠展示
+      stepColumns() {
+        const list = Array.isArray(this.routeList) ? this.routeList : [];
+        const cols = [];
+        list.forEach((item, flatIndex) => {
+          const isParallel =
+            !!item &&
+            (item.isParallel === 1 ||
+              item.isParallel === '1' ||
+              item.isParallel === true);
+          const cur = cols[cols.length - 1];
+          if (isParallel && cur && cur.parallel) {
+            cur.items.push({ item, flatIndex });
+          } else {
+            cols.push({
+              parallel: isParallel,
+              items: [{ item, flatIndex }]
+            });
+          }
+        });
+        return cols;
+      },
+      // 流程图整体高度 = 最大并行列的节点数 * 节点行高
+      stepsRowHeight() {
+        const maxRows = this.stepColumns.reduce(
+          (m, col) => Math.max(m, col.items.length),
+          1
+        );
+        return maxRows * this.stepNodeH;
+      }
+    },
     methods: {
+      // 测量连接线区域的实际宽度(自适应布局下需实测)
+      measureConnectorWidth() {
+        this.$nextTick(() => {
+          const el = this.$el && this.$el.querySelector('.route-connector');
+          if (!el) {
+            return;
+          }
+          const w = Math.max(
+            Math.round(el.getBoundingClientRect().width),
+            this.stepConnectorW
+          );
+          if (w !== this.connectorViewW) {
+            this.connectorViewW = w;
+          }
+        });
+      },
+      // 计算两列之间的连线:左列每个节点与右列每个节点全连接(分叉/合并)
+      connectorLines(leftCol, rightCol) {
+        const total = this.stepsRowHeight;
+        const h = this.stepNodeH;
+        const w = this.connectorViewW;
+        const midX = w / 2;
+        const centers = (n) => {
+          const offset = (total - n * h) / 2;
+          return Array.from({ length: n }, (_, i) => offset + (i + 0.5) * h);
+        };
+        const leftYs = centers(leftCol.items.length);
+        const rightYs = centers(rightCol.items.length);
+        const lines = [];
+        leftYs.forEach((y1) => {
+          rightYs.forEach((y2) => {
+            // 直角折线:水平 → 垂直 → 水平;同行时垂直段退化为直线
+            lines.push({
+              d: `M 0 ${y1} L ${midX} ${y1} L ${midX} ${y2} L ${w} ${y2}`
+            });
+          });
+        });
+        return lines;
+      },
+
       getTaskFn() {
         getTaskInstanceList(this.routeObj.id).then((res) => {
           this.routeList = res;
@@ -131,6 +252,7 @@
 
           this.activeIndex = index;
           this.$refs.pickListRef.getList([this.routeObj.id]);
+          this.measureConnectorWidth();
         });
       },
 
@@ -154,6 +276,17 @@
 
     created() {
       this.getTaskFn();
+    },
+
+    mounted() {
+      this._routeResizeHandler = () => this.measureConnectorWidth();
+      window.addEventListener('resize', this._routeResizeHandler);
+    },
+
+    destroyed() {
+      if (this._routeResizeHandler) {
+        window.removeEventListener('resize', this._routeResizeHandler);
+      }
     }
   };
 </script>
@@ -173,4 +306,168 @@
       font-weight: bold;
     }
   }
+
+  .route-flow {
+    display: flex;
+    align-items: center;
+    min-height: 88px;
+    padding: 12px 0 16px;
+    box-sizing: border-box;
+    overflow-x: auto;
+  }
+
+  .route-flow-unit {
+    display: flex;
+    align-items: center;
+    height: 100%;
+    flex: 1 1 0;
+    min-width: 140px;
+  }
+  .route-flow-unit:nth-of-type(1) {
+    flex: 0 0 auto !important;
+    min-width: 0 !important;
+  }
+  .route-connector {
+    flex: 1 1 0;
+    min-width: 0;
+    display: flex;
+    align-items: center;
+    height: 100%;
+  }
+
+  .route-connector-svg {
+    display: block;
+    width: 100%;
+    height: 100%;
+  }
+
+  .route-connector-line {
+    stroke: #909399;
+    stroke-width: 1.5;
+    fill: none;
+    vector-effect: non-scaling-stroke;
+  }
+
+  .route-col {
+    flex: 0 0 auto;
+    margin: 0 auto;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    height: 100%;
+  }
+
+  .route-node-row {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100px;
+    box-sizing: border-box;
+  }
+
+  .route-node-row.is-parallel {
+    margin-bottom: 2px;
+  }
+
+  .route-node-row.is-parallel:last-child {
+    margin-bottom: 0;
+  }
+
+  .route-node {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    min-width: 0;
+    max-width: none;
+    min-height: 0;
+    padding: 2px 8px;
+    background: #ffffff;
+    border: 2px solid #c0c4cc;
+    border-radius: 2px;
+    cursor: pointer;
+    box-sizing: border-box;
+    overflow: hidden;
+    transition: border-color 0.2s, box-shadow 0.2s;
+
+    &:hover {
+      border-color: #157a2c;
+    }
+  }
+
+  .route-node.is-done {
+    border-color: #1890ff;
+
+    .route-node-name {
+      color: #1890ff;
+    }
+  }
+
+  .route-node.is-current {
+    border-color: #1890ff;
+
+    .route-node-name {
+      color: #1890ff;
+      font-weight: 700;
+    }
+  }
+
+  .route-node.is-error {
+    border-color: #f56c6c;
+
+    .route-node-name {
+      color: #f56c6c;
+    }
+  }
+
+  // 并行工序节点(isParallel == 1):更紧凑
+  .route-node.is-compact {
+    width: 100%;
+    height: 100%;
+    min-width: 0;
+    max-width: none;
+    min-height: 0;
+    padding: 1px 6px;
+    overflow: hidden;
+
+    .route-node-name {
+      font-size: 12px;
+    }
+
+    .route-node-team {
+      font-size: 10px;
+    }
+  }
+
+  .route-node-name {
+    font-size: 12px;
+    font-weight: 500;
+    line-height: 1.3;
+    color: #303133;
+    text-align: center;
+    word-break: break-all;
+  }
+
+  .route-node-team {
+    margin-top: 2px;
+    color: #606266;
+    font-size: 10px;
+    line-height: 1.2;
+    text-align: center;
+    word-break: break-all;
+  }
+
+  .route-node-tag {
+    display: inline-block;
+    margin-left: 4px;
+    padding: 0 6px;
+    font-size: 11px;
+    line-height: 16px;
+    color: #ffffff;
+    background: #1890ff;
+    border-radius: 8px;
+    vertical-align: middle;
+  }
 </style>

+ 50 - 13
src/views/produce/components/workPlan/components/inspectionProjectReport.vue

@@ -62,10 +62,9 @@
       <template v-slot:sampleNoQualifiedNumber="{ row }">
         <el-input
           v-model="row.sampleNoQualifiedNumber"
-          @input="formSampleNoQualifiedNumberChange"
-          placeholder="请输入"
+          placeholder="自动取值(质检项中最大不合格数)"
           type="number"
-          :disabled="type == 'detail'"
+          disabled
         >
           <template slot="append">{{ form.measureUnit }}</template>
         </el-input>
@@ -214,6 +213,7 @@
           placeholder="请选择"
           style="width: 100%"
           :disabled="type == 'detail'"
+          @change="handleQualityResultChange(row, $index)"
         >
           <el-option
             v-for="item in qualityResultsList"
@@ -756,6 +756,30 @@
         return this.form.sampleQuantity;
       }
     },
+    watch: {
+      // 任意质检项的不合格数变更时,自动同步最大不合格数到样品不合格数
+      tableData: {
+        deep: true,
+        handler(list) {
+          if (!Array.isArray(list) || !list.length) {
+            this.$set(this.form, 'sampleNoQualifiedNumber', 0);
+          } else {
+            const maxNoQualified = list.reduce(
+              (max, item) =>
+                Math.max(max, Number(item.noQualifiedQuantity) || 0),
+              0
+            );
+            this.$set(this.form, 'sampleNoQualifiedNumber', maxNoQualified);
+          }
+          const total = Number(this.form.sampleQuantity) || 0;
+          this.$set(
+            this.form,
+            'sampleQualifiedNumber',
+            Math.max(total - (Number(this.form.sampleNoQualifiedNumber) || 0), 0)
+          );
+        }
+      }
+    },
 
     created() {
       this.requestDict('质检计划类型');
@@ -887,16 +911,14 @@
             ]);
           });
         }
-        this.form.sampleQualifiedNumber = this.form.sampleQuantity;
-        this.form.sampleNoQualifiedNumber = 0;
-      },
-      formSampleNoQualifiedNumberChange() {
-        if (+this.form.sampleNoQualifiedNumber < 0) {
-          this.form.sampleNoQualifiedNumber = 0;
-        }
-        if (+this.form.sampleNoQualifiedNumber > this.form.sampleQuantity) {
-          this.form.sampleNoQualifiedNumber = this.form.sampleQuantity;
-        }
+        // 样品数变化后,重新计算合格数/不合格数
+        this.form.sampleNoQualifiedNumber = this.tableData.length
+          ? this.tableData.reduce(
+              (max, item) =>
+                Math.max(max, Number(item.noQualifiedQuantity) || 0),
+              0
+            )
+          : 0;
         this.form.sampleQualifiedNumber =
           this.form.sampleQuantity - this.form.sampleNoQualifiedNumber;
       },
@@ -906,6 +928,21 @@
           this.$set(this.tableData[index], 'qualityResults', 1);
         });
       },
+      // 切换质检结果(合格/不合格)时,若合格数与不合格数其一为 0,则互换两个数值
+      handleQualityResultChange(row, index) {
+        const qualified = Number(row.qualifiedQuantity) || 0;
+        const noQualified = Number(row.noQualifiedQuantity) || 0;
+        // 切换为合格:noQualifiedQuantity -> qualifiedQuantity
+        if (row.qualityResults == 1 && qualified === 0 && noQualified > 0) {
+          this.$set(this.tableData[index], 'qualifiedQuantity', noQualified);
+          this.$set(this.tableData[index], 'noQualifiedQuantity', 0);
+        }
+        // 切换为不合格:qualifiedQuantity -> noQualifiedQuantity
+        if (row.qualityResults == 2 && noQualified === 0 && qualified > 0) {
+          this.$set(this.tableData[index], 'qualifiedQuantity', 0);
+          this.$set(this.tableData[index], 'noQualifiedQuantity', qualified);
+        }
+      },
 
       handleClose() {
         this.visible = false;

+ 36 - 30
src/views/produce/components/workPlan/components/sampleListDialog.vue

@@ -10,17 +10,22 @@
     :close-on-click-modal="false"
     :close-on-press-escape="false"
   >
-    <div class="two-layout">
-      <el-row :gutter="10" class="full-row">
-        <el-col :span="12" class="full-col">
-          <div class="pane">
-            <div class="pane-title">质检</div>
-            <div class="table-wrapper">
-              <el-table
-                :data="tableData"
-                :default-expand-all="true"
-                height="calc(80vh - 120px)"
-              >
+    <ele-split-layout
+      class="two-layout"
+      width="70%"
+      :min-size="280"
+      :max-size="0.85"
+      space="6px"
+      :resizable="true"
+    >
+      <div class="pane">
+        <div class="pane-title">质检</div>
+        <div class="table-wrapper">
+          <el-table
+            :data="tableData"
+            :default-expand-all="true"
+            height="100%"
+          >
                 <el-table-column type="expand">
                   <template slot-scope="props">
                     <div style="padding: 10px">
@@ -231,18 +236,15 @@
                 </el-button>
               </div>
             </div>
-          </div>
-        </el-col>
+      </div>
+      <template #content>
+        <div class="pane">
+          <div class="pane-title">工艺文件</div>
 
-        <el-col :span="12" class="full-col">
-          <div class="pane">
-            <div class="pane-title">工艺文件</div>
-
-            <wokePopupList ref="wokePopupListRef"></wokePopupList>
-          </div>
-        </el-col>
-      </el-row>
-    </div>
+          <wokePopupList ref="wokePopupListRef"></wokePopupList>
+        </div>
+      </template>
+    </ele-split-layout>
 
     <!-- <el-table :data="tableData" :default-expand-all="true">
       <el-table-column type="expand">
@@ -780,18 +782,20 @@
 
   .two-layout {
     height: 80vh;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
   }
 
-  .full-row {
-    flex: 1;
-    height: 100%;
-    min-height: 0;
+  // ele-admin 默认 justify-content: flex-end 把 side 推到 wrap 右端,
+  // 改成 flex-start 让左侧 pane 靠左
+  ::v-deep .ele-split-panel-wrap {
+    justify-content: flex-start;
+    .ele-split-panel-side {
+      width: 100%;
+    }
   }
 
-  .full-col {
+  // 让两个 pane 撑满 ele-split-layout 的 side / body 容器
+  ::v-deep .ele-split-panel-side,
+  ::v-deep .ele-split-panel-body {
     height: 100%;
     min-height: 0;
   }
@@ -804,7 +808,9 @@
 
     display: flex;
     flex-direction: column;
+    min-width: 0;
     min-height: 0;
+    overflow: hidden;
   }
 
   /* 表格父级 */

+ 312 - 15
src/views/produceOrder/components/details/index.vue

@@ -31,21 +31,64 @@
         <!-- //详细信息 -->
         <Info :workOrderInfo="workOrderInfo"></Info>
 
-        <el-steps
-          :active="activeIndex"
-          space="20px"
-          align-center
-          style="margin-top: 18px"
+        <div
+          class="route-flow"
+          :style="{ height: stepsRowHeight + 'px' }"
         >
-          <el-step
-            v-for="(item, index) in routeList"
-            :key="index"
-            :title="item.taskTypeName"
-            @click.native="handIdx(index, item)"
-            :description="desIndex == index ? '此处' : ''"
-            :class="desIndex == index ? 'active' : ''"
-          ></el-step>
-        </el-steps>
+          <div
+            v-for="(col, colIndex) in stepColumns"
+            :key="`route-unit-${colIndex}`"
+            class="route-flow-unit"
+          >
+            <div v-if="colIndex > 0" class="route-connector">
+              <svg
+                class="route-connector-svg"
+                :viewBox="`0 0 ${connectorViewW} ${stepsRowHeight}`"
+                preserveAspectRatio="none"
+              >
+                <path
+                  v-for="(line, li) in connectorLines(
+                    stepColumns[colIndex - 1],
+                    col
+                  )"
+                  :key="li"
+                  :d="line.d"
+                  class="route-connector-line"
+                />
+              </svg>
+            </div>
+            <div class="route-col">
+              <div
+                v-for="row in col.items"
+                :key="row.flatIndex"
+                class="route-node-row"
+                :class="{ 'is-parallel': row.item.isParallel == 1 }"
+                :style="{ height: stepNodeH + 'px' }"
+              >
+                <div
+                  class="route-node"
+                  :class="{
+                    'is-current': row.flatIndex === desIndex,
+                    'is-error': row.flatIndex === errorIndex,
+                    'is-done': row.flatIndex < activeIndex,
+                    'is-compact': row.item.isParallel == 1
+                  }"
+                  :title="row.item.taskTypeName"
+                  @click="handIdx(row.flatIndex, row.item)"
+                >
+                  <div class="route-node-name">
+                    {{ row.item.taskTypeName
+                    }}<span
+                      v-if="row.flatIndex === desIndex"
+                      class="route-node-tag"
+                      >此处</span
+                    >
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
 
         <el-tabs
           v-model="detailActiveTab"
@@ -146,6 +189,7 @@
         routeList: [],
         activeIndex: 0,
         desIndex: 0,
+        errorIndex: 10000,
         curTaskObj: null,
         newId: '',
         routeObj: {
@@ -154,10 +198,46 @@
         sourceTaskId: '',
         /** 任务数据就绪后递增,强制各 Tab 子组件重挂载以重新拉数 */
         tabsRenderKey: 0,
-        detailActiveTab: 'pick'
+        detailActiveTab: 'pick',
+        stepNodeH: 38, // 每个工序节点的行高
+        stepConnectorW: 12, // 列间连接线 SVG 的最小宽度
+        connectorViewW: 56 // 连接线 SVG 的实际宽度(自适应测量)
       };
     },
 
+    computed: {
+      // 按 isParallel 连续段把工序拆分成"列",并行工序同列垂直堆叠展示
+      stepColumns() {
+        const list = Array.isArray(this.routeList) ? this.routeList : [];
+        const cols = [];
+        list.forEach((item, flatIndex) => {
+          const isParallel =
+            !!item &&
+            (item.isParallel === 1 ||
+              item.isParallel === '1' ||
+              item.isParallel === true);
+          const cur = cols[cols.length - 1];
+          if (isParallel && cur && cur.parallel) {
+            cur.items.push({ item, flatIndex });
+          } else {
+            cols.push({
+              parallel: isParallel,
+              items: [{ item, flatIndex }]
+            });
+          }
+        });
+        return cols;
+      },
+      // 流程图整体高度 = 最大并行列的节点数 * 节点行高
+      stepsRowHeight() {
+        const maxRows = this.stepColumns.reduce(
+          (m, col) => Math.max(m, col.items.length),
+          1
+        );
+        return maxRows * this.stepNodeH;
+      }
+    },
+
     methods: {
       handleClose() {
         this.activeIndex = 0;
@@ -167,6 +247,47 @@
       handleFull() {
         this.isFullscreen = !this.isFullscreen;
         this.$forceUpdate();
+        this.measureConnectorWidth();
+      },
+
+      // 测量连接线区域的实际宽度(自适应布局下需实测)
+      measureConnectorWidth() {
+        this.$nextTick(() => {
+          const el = this.$el && this.$el.querySelector('.route-connector');
+          if (!el) {
+            return;
+          }
+          const w = Math.max(
+            Math.round(el.getBoundingClientRect().width),
+            this.stepConnectorW
+          );
+          if (w !== this.connectorViewW) {
+            this.connectorViewW = w;
+          }
+        });
+      },
+      // 计算两列之间的连线:左列每个节点与右列每个节点全连接(分叉/合并)
+      connectorLines(leftCol, rightCol) {
+        const total = this.stepsRowHeight;
+        const h = this.stepNodeH;
+        const w = this.connectorViewW;
+        const midX = w / 2;
+        const centers = (n) => {
+          const offset = (total - n * h) / 2;
+          return Array.from({ length: n }, (_, i) => offset + (i + 0.5) * h);
+        };
+        const leftYs = centers(leftCol.items.length);
+        const rightYs = centers(rightCol.items.length);
+        const lines = [];
+        leftYs.forEach((y1) => {
+          rightYs.forEach((y2) => {
+            // 直角折线:水平 → 垂直 → 水平;同行时垂直段退化为直线
+            lines.push({
+              d: `M 0 ${y1} L ${midX} ${y1} L ${midX} ${y2} L ${w} ${y2}`
+            });
+          });
+        });
+        return lines;
       },
 
       open(row) {
@@ -214,6 +335,7 @@
           })
           .finally(() => {
             this.tabsRenderKey += 1;
+            this.measureConnectorWidth();
           });
       },
 
@@ -246,6 +368,17 @@
           this.sourceTaskId = this.routeList[this.desIndex]?.sourceTaskId || '';
         }
       }
+    },
+
+    mounted() {
+      this._routeResizeHandler = () => this.measureConnectorWidth();
+      window.addEventListener('resize', this._routeResizeHandler);
+    },
+
+    destroyed() {
+      if (this._routeResizeHandler) {
+        window.removeEventListener('resize', this._routeResizeHandler);
+      }
     }
   };
 </script>
@@ -286,4 +419,168 @@
     border-color: #ffa929;
     color: #ffffff; /* 图标文字颜色 */
   }
+
+  .route-flow {
+    display: flex;
+    align-items: center;
+    min-height: 88px;
+    padding: 12px 0 16px;
+    box-sizing: border-box;
+    overflow-x: auto;
+  }
+
+  .route-flow-unit {
+    display: flex;
+    align-items: center;
+    height: 100%;
+    flex: 1 1 0;
+    min-width: 140px;
+  }
+  .route-flow-unit:nth-of-type(1) {
+    flex: 0 0 auto !important;
+    min-width: 0 !important;
+  }
+  .route-connector {
+    flex: 1 1 0;
+    min-width: 0;
+    display: flex;
+    align-items: center;
+    height: 100%;
+  }
+
+  .route-connector-svg {
+    display: block;
+    width: 100%;
+    height: 100%;
+  }
+
+  .route-connector-line {
+    stroke: #909399;
+    stroke-width: 1.5;
+    fill: none;
+    vector-effect: non-scaling-stroke;
+  }
+
+  .route-col {
+    flex: 0 0 auto;
+    margin: 0 auto;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    height: 100%;
+  }
+
+  .route-node-row {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100px;
+    box-sizing: border-box;
+  }
+
+  .route-node-row.is-parallel {
+    margin-bottom: 2px;
+  }
+
+  .route-node-row.is-parallel:last-child {
+    margin-bottom: 0;
+  }
+
+  .route-node {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    min-width: 0;
+    max-width: none;
+    min-height: 0;
+    padding: 2px 8px;
+    background: #ffffff;
+    border: 2px solid #c0c4cc;
+    border-radius: 2px;
+    cursor: pointer;
+    box-sizing: border-box;
+    overflow: hidden;
+    transition: border-color 0.2s, box-shadow 0.2s;
+
+    &:hover {
+      border-color: #157a2c;
+    }
+  }
+
+  .route-node.is-done {
+    border-color: #1890ff;
+
+    .route-node-name {
+      color: #1890ff;
+    }
+  }
+
+  .route-node.is-current {
+    border-color: #1890ff;
+
+    .route-node-name {
+      color: #1890ff;
+      font-weight: 700;
+    }
+  }
+
+  .route-node.is-error {
+    border-color: #f56c6c;
+
+    .route-node-name {
+      color: #f56c6c;
+    }
+  }
+
+  // 并行工序节点(isParallel == 1):更紧凑
+  .route-node.is-compact {
+    width: 100%;
+    height: 100%;
+    min-width: 0;
+    max-width: none;
+    min-height: 0;
+    padding: 1px 6px;
+    overflow: hidden;
+
+    .route-node-name {
+      font-size: 12px;
+    }
+
+    .route-node-team {
+      font-size: 10px;
+    }
+  }
+
+  .route-node-name {
+    font-size: 12px;
+    font-weight: 500;
+    line-height: 1.3;
+    color: #303133;
+    text-align: center;
+    word-break: break-all;
+  }
+
+  .route-node-team {
+    margin-top: 2px;
+    color: #606266;
+    font-size: 10px;
+    line-height: 1.2;
+    text-align: center;
+    word-break: break-all;
+  }
+
+  .route-node-tag {
+    display: inline-block;
+    margin-left: 4px;
+    padding: 0 6px;
+    font-size: 11px;
+    line-height: 16px;
+    color: #ffffff;
+    background: #1890ff;
+    border-radius: 8px;
+    vertical-align: middle;
+  }
 </style>

+ 18 - 16
src/views/produceOrder/workReport.vue

@@ -87,6 +87,7 @@
                     v-for="row in col.items"
                     :key="row.flatIndex"
                     class="route-node-row"
+                    :class="{ 'is-parallel': row.item.isParallel == 1 }"
                     :style="{ height: stepNodeH + 'px' }"
                   >
                     <div
@@ -101,7 +102,12 @@
                       @click="handIdx(row.flatIndex, row.item)"
                     >
                       <div class="route-node-name">
-                        {{ row.item.taskTypeName }}
+                        {{ row.item.taskTypeName
+                        }}<span
+                          v-if="row.flatIndex === desIndex"
+                          class="route-node-tag"
+                          >此处</span
+                        >
                       </div>
                       <div
                         v-if="showTaskTeamName(row.item)"
@@ -110,10 +116,6 @@
                         {{ getTaskTeamName(row.item) }}
                       </div>
                     </div>
-                    <span
-                      v-if="row.flatIndex === desIndex"
-                      class="route-node-tag"
-                    >此处</span>
                   </div>
                 </div>
               </div>
@@ -1937,7 +1939,6 @@
   }
 
   .route-node-row {
-    position: relative;
     display: flex;
     align-items: center;
     justify-content: center;
@@ -1945,6 +1946,14 @@
     box-sizing: border-box;
   }
 
+  .route-node-row.is-parallel {
+    margin-bottom: 2px;
+  }
+
+  .route-node-row.is-parallel:last-child {
+    margin-bottom: 0;
+  }
+
   .route-node {
     display: flex;
     flex-direction: column;
@@ -2012,10 +2021,6 @@
       font-size: 10px;
     }
 
-    // 并行节点激活时不显示"此处"标签,避免撑高
-    .route-node-tag {
-      display: none;
-    }
   }
 
   .route-node-name {
@@ -2037,18 +2042,15 @@
   }
 
   .route-node-tag {
-    position: absolute;
-    top: 100%;
-    left: 50%;
-    transform: translateX(-50%);
-    margin-top: 6px;
+    display: inline-block;
+    margin-left: 4px;
     padding: 0 6px;
     font-size: 11px;
     line-height: 16px;
     color: #ffffff;
     background: #1890ff;
     border-radius: 8px;
-    white-space: nowrap;
+    vertical-align: middle;
   }
 
   ::v-deep .el-tabs__content {

+ 9 - 2
src/views/productionPlan/components/newFactoryProductionScheduling.vue

@@ -1214,8 +1214,15 @@
           rawExecutionTeamId: '',
           executionObjectType: '',
           executionObjectName: '',
-          executionTeamLeader: '',
-          executionTeamLeaderId: '',
+          executionTeamLeader:
+            item.executionTeamLeader ||
+            item.dispatchTeamLeader ||
+            item.teamLeaderUserName ||
+            item.leaderUserName ||
+            item.firstTaskTeamLeader ||
+            item.executionLeaderName ||
+            '',
+          executionTeamLeaderId: item.executionTeamLeaderId || '',
           executionTeamName: '',
           executionStartTime,
           executionEndTime,

+ 28 - 9
src/views/productionPlan/components/newFactoryProductionScheduling/TaskConfigPanel.vue

@@ -50,7 +50,7 @@
         <div class="dispatch-process-meta">
           工序编码:{{ taskCodeDisplay(activeTask) }},所属班组:{{
             dispatchTeamDisplay(activeTask)
-          }}
+          }},班组长:{{ dispatchTeamLeaderDisplay(activeTask) }}
         </div>
 
         <div class="dispatch-toolbar">
@@ -697,6 +697,14 @@
     'teamNames',
     'executionTeamName'
   ];
+  const DISPATCH_TEAM_LEADER_NAME_KEYS = [
+    'dispatchTeamLeader',
+    'executionTeamLeader',
+    'teamLeaderUserName',
+    'leaderUserName',
+    'firstTaskTeamLeader',
+    'executionLeaderName'
+  ];
 
   export default {
     name: 'TaskConfigPanel',
@@ -1915,8 +1923,8 @@
           this.$set(task, 'executionObjectType', '');
           this.$set(task, 'executionObjectName', '');
           this.$set(task, 'executionTeamName', '');
-          this.$set(task, 'executionTeamLeader', '');
-          this.$set(task, 'executionTeamLeaderId', '');
+          // this.$set(task, 'executionTeamLeader', '');
+          // this.$set(task, 'executionTeamLeaderId', '');
         });
       },
       createDispatchTaskRow(row) {
@@ -1960,8 +1968,8 @@
         this.$set(task, 'executionObjectType', '');
         this.$set(task, 'executionObjectName', '');
         this.$set(task, 'executionTeamName', '');
-        this.$set(task, 'executionTeamLeader', '');
-        this.$set(task, 'executionTeamLeaderId', '');
+        // this.$set(task, 'executionTeamLeader', '');
+        // this.$set(task, 'executionTeamLeaderId', '');
       },
       clearDispatchTaskBinding(row, task = this.getDispatchRowTask(row)) {
         if (!task) {
@@ -1977,8 +1985,8 @@
         this.$set(task, 'executionObjectType', '');
         this.$set(task, 'executionObjectName', '');
         this.$set(task, 'executionTeamName', '');
-        this.$set(task, 'executionTeamLeader', '');
-        this.$set(task, 'executionTeamLeaderId', '');
+        // this.$set(task, 'executionTeamLeader', '');
+        // this.$set(task, 'executionTeamLeaderId', '');
       },
       clearDispatchQuantityInput(row) {
         this.clearDispatchObject(row);
@@ -2072,8 +2080,8 @@
         this.$set(task, 'executionObjectType', '');
         this.$set(task, 'executionObjectName', '');
         this.$set(task, 'executionTeamName', '');
-        this.$set(task, 'executionTeamLeader', '');
-        this.$set(task, 'executionTeamLeaderId', '');
+        // this.$set(task, 'executionTeamLeader', '');
+        // this.$set(task, 'executionTeamLeaderId', '');
       },
       clearDispatchStartTime(row) {
         if (row) {
@@ -2690,6 +2698,17 @@
           ]) || '所有班组'
         );
       },
+
+      dispatchTeamLeaderDisplay(row = this.activeTask) {
+        const plan = this.currentPlan || {};
+        return (
+          this.pickDispatchSingleValue([
+            { source: row || {}, keys: DISPATCH_TEAM_LEADER_NAME_KEYS },
+            { source: plan, keys: DISPATCH_TEAM_LEADER_NAME_KEYS }
+          ]) || '-'
+        );
+      },
+
       handleDispatchPageSizeChange(size) {
         this.dispatchPageSize = size;
         this.dispatchPage = 1;