Sfoglia il codice sorgente

修改布点的bug

695593266@qq.com 2 mesi fa
parent
commit
1b7a8bb885

+ 244 - 84
src/views/productionPlan/components/planDotLine.vue

@@ -1,7 +1,7 @@
 <template>
   <ele-modal
     v-if="visible"
-    custom-class="ele-dialog-form long-dialog-form"
+    custom-class="ele-dialog-form long-dialog-form plan-dot-line-dialog"
     :centered="true"
     :append-to-body="true"
     :visible.sync="dialogVisible"
@@ -9,8 +9,10 @@
     width="70%"
     :maxable="true"
     :resizable="true"
+    :fullscreen.sync="modalFullscreen"
     :title="title"
     @close="onModalClose"
+    @opened="onModalOpened"
   >
     <div class="plan-dot-line">
       <div class="top-route">
@@ -19,26 +21,22 @@
           v-if="taskList.length === 0"
           description="暂无工艺路线"
         ></el-empty>
-        <div v-else class="route-line">
-          <div
+        <el-steps
+          v-else
+          :active="routeStepsActive"
+          space="20px"
+          align-center
+          finish-status="success"
+          class="route-steps"
+        >
+          <el-step
             v-for="(item, index) in taskList"
-            :key="`route-seg-${item._taskKey}`"
-            class="route-segment"
-          >
-            <span
-              class="route-node"
-              :class="{ 'route-node--done': isRouteItemDone(item) }"
-            >
-              {{ item.taskName || item.name || `工艺${index + 1}` }}
-            </span>
-            <span
-              v-if="index < taskList.length - 1"
-              class="route-arrow"
-              aria-hidden="true"
-              >→</span
-            >
-          </div>
-        </div>
+            :key="`route-step-${item._taskKey}`"
+            :title="routeStepTitle(item, index)"
+            :class="{ active: routeDesIndex === index }"
+            v-bind="routeErrorIndex === index ? { status: 'error' } : {}"
+          ></el-step>
+        </el-steps>
       </div>
       <div class="config-panel">
         <div class="panel-title">工艺配置</div>
@@ -48,17 +46,20 @@
         ></el-empty>
         <div v-else class="task-config-table-wrap">
           <el-table
+            :key="modalFullscreen ? 'fs' : 'nm'"
+            ref="configTable"
+            :height="modalFullscreen ? tableMaxHeight : undefined"
+            :max-height="modalFullscreen ? undefined : tableMaxHeight"
             :data="taskList"
             border
             size="small"
             row-key="_taskKey"
-            max-height="420"
             class="config-table"
           >
             <el-table-column
               type="index"
               label="序号"
-              width="48"
+              width="60"
               align="center"
             />
             <el-table-column
@@ -81,6 +82,7 @@
                   placeholder="执行模式"
                   clearable
                   class="config-table-control"
+                  @change="onExecutionTypeChange(row)"
                 >
                   <el-option
                     v-for="opt in executionTypeOptions"
@@ -98,6 +100,7 @@
                   placeholder="请选择班组"
                   clearable
                   filterable
+                  :disabled="isTeamSelectDisabled(row)"
                   class="config-table-control"
                   @change="onExecutionTeamChange(row)"
                 >
@@ -148,6 +151,12 @@
     </div>
     <div slot="footer">
       <el-button size="mini" @click="onModalClose">取消</el-button>
+      <el-button size="mini" type="warning" @click="handleClearAll"
+        >一键清空</el-button
+      >
+      <el-button size="mini" type="success" @click="handleCache"
+        >缓存</el-button
+      >
       <el-button size="mini" type="primary" @click="handleSave">保存</el-button>
     </div>
   </ele-modal>
@@ -205,10 +214,86 @@
         teamOptions: [],
         currentPlan: null,
         planRoutingPayload: null,
-        executionTypeOptions: EXEC_TYPE_OPTIONS
+        executionTypeOptions: EXEC_TYPE_OPTIONS,
+        /** 与 ele-modal 全屏按钮同步,用于表格高度适配 */
+        modalFullscreen: false,
+        /** 普通弹窗下动态限制表格最大高度,避免写死 420 */
+        tableMaxHeight: 420,
+        /** 工艺路线步骤 error 状态(-1 表示无) */
+        routeErrorIndex: -1
       };
     },
+    computed: {
+      /** 当前激活步骤:首个未配置执行模式的工序;全部已配置则为步骤总数(全部 finish) */
+      routeStepsActive() {
+        const list = this.taskList;
+        if (!list.length) return 0;
+        for (let i = 0; i < list.length; i++) {
+          if (!this.isRouteItemDone(list[i])) return i;
+        }
+        return list.length;
+      },
+      /** 用于 :class="active" 高亮,与 active 同步(全部完成时高亮最后一环) */
+      routeDesIndex() {
+        const list = this.taskList;
+        if (!list.length) return -1;
+        const a = this.routeStepsActive;
+        return a >= list.length ? list.length - 1 : a;
+      }
+    },
+    watch: {
+      modalFullscreen() {
+        // 先算好高度,key 变化会重建 el-table,重建后再 doLayout
+        this.calcTableHeight();
+        this.$nextTick(() => {
+          this.$refs.configTable?.doLayout?.();
+          // 全屏动画结束后再修正一次
+          setTimeout(() => {
+            this.calcTableHeight();
+            this.$nextTick(() => {
+              this.$refs.configTable?.doLayout?.();
+            });
+          }, 400);
+        });
+      }
+    },
+    beforeDestroy() {
+      window.removeEventListener('resize', this.calcTableHeight);
+    },
     methods: {
+      onModalOpened() {
+        window.addEventListener('resize', this.calcTableHeight);
+        this.$nextTick(() => {
+          this.calcTableHeight();
+        });
+      },
+      calcTableHeight() {
+        if (this.modalFullscreen) {
+          const tableEl = this.$refs.configTable?.$el;
+          const footerEl = tableEl
+            ?.closest('.el-dialog')
+            ?.querySelector('.el-dialog__footer');
+          const footerH = footerEl ? footerEl.offsetHeight : 52;
+          const wrapEl = tableEl?.closest('.task-config-table-wrap');
+          if (wrapEl) {
+            const top = wrapEl.getBoundingClientRect().top;
+            const vh = window.innerHeight;
+            this.tableMaxHeight = Math.max(
+              200,
+              Math.floor(vh - top - footerH - 16)
+            );
+          } else {
+            const vh = window.innerHeight || 800;
+            this.tableMaxHeight = Math.max(200, vh - 260);
+          }
+        } else {
+          const vh = window.innerHeight || 800;
+          this.tableMaxHeight = Math.max(320, Math.floor(vh * 0.52));
+        }
+        this.$nextTick(() => {
+          this.$refs.configTable?.doLayout?.();
+        });
+      },
       async open(data) {
         this.currentPlan = data || null;
         this.planRoutingPayload = null;
@@ -255,16 +340,19 @@
       },
 
       normalizeDetailRow(item, index) {
+        const execType = toSafeNumber(item.executionType) ?? EXEC_TYPE.HOMEMADE;
+        const noTeam =
+          execType === EXEC_TYPE.ENTRUST || execType === EXEC_TYPE.OUTSOURCE;
         return {
           ...item,
           _taskKey: item.id ?? `detail-${item.taskId ?? index}`,
           executionStartTime: formatDateTime(item.executionStartTime),
           executionEndTime: formatDateTime(item.executionEndTime),
-          executionType: toSafeNumber(item.executionType) ?? EXEC_TYPE.HOMEMADE,
-          executionTeamId: item.executionTeamId ?? '',
-          executionTeamLeader: item.executionTeamLeader ?? '',
-          executionTeamLeaderId: item.executionTeamLeaderId ?? '',
-          executionTeamName: item.executionTeamName ?? ''
+          executionType: execType,
+          executionTeamId: noTeam ? '' : item.executionTeamId ?? '',
+          executionTeamLeader: noTeam ? '' : item.executionTeamLeader ?? '',
+          executionTeamLeaderId: noTeam ? '' : item.executionTeamLeaderId ?? '',
+          executionTeamName: noTeam ? '' : item.executionTeamName ?? ''
         };
       },
 
@@ -295,6 +383,24 @@
         };
       },
 
+      isTeamSelectDisabled(row) {
+        const t = Number(row.executionType);
+        return t === EXEC_TYPE.ENTRUST || t === EXEC_TYPE.OUTSOURCE;
+      },
+
+      clearExecutionTeamFields(row) {
+        this.$set(row, 'executionTeamId', '');
+        this.$set(row, 'executionTeamName', '');
+        this.$set(row, 'executionTeamLeader', '');
+        this.$set(row, 'executionTeamLeaderId', '');
+      },
+
+      onExecutionTypeChange(row) {
+        if (this.isTeamSelectDisabled(row)) {
+          this.clearExecutionTeamFields(row);
+        }
+      },
+
       onExecutionTeamChange(row) {
         const team = this.teamOptions.find((t) => t.id === row.executionTeamId);
         row.executionTeamName = team?.name ?? '';
@@ -302,6 +408,16 @@
         row.executionTeamLeaderId = team?.leaderUserId ?? '';
       },
 
+      handleClearAll() {
+        this.taskList.forEach((row) => {
+          this.$set(row, 'executionType', EXEC_TYPE.HOMEMADE);
+          this.clearExecutionTeamFields(row);
+          this.$set(row, 'executionStartTime', '');
+          this.$set(row, 'executionEndTime', '');
+        });
+        this.$message.success('已清空');
+      },
+
       async loadTeamOptions() {
         try {
           const factoryId = this.$store.state.user.info.factoryId;
@@ -323,6 +439,12 @@
         return row.executionType != null && row.executionType !== '';
       },
 
+      routeStepTitle(item, index) {
+        return (
+          item.taskTypeName || item.taskName || item.name || `工艺${index + 1}`
+        );
+      },
+
       handleTimeChange(row, changeKey) {
         if (isEndBeforeStart(row.executionStartTime, row.executionEndTime)) {
           this.$message.warning('执行结束时间不能小于执行开始时间');
@@ -394,7 +516,8 @@
         return true;
       },
 
-      buildSavePayload() {
+      /** @param {0|1} isCache 缓存传 1,保存传 0 */
+      buildSavePayload(isCache = 0) {
         const plan = this.currentPlan;
         const head = this.planRoutingPayload || {};
 
@@ -428,6 +551,7 @@
           detailList,
           fileParam: head.fileParam ?? [],
           id: head.id,
+          isCache,
           planCode: plan?.code ?? head.planCode,
           planId: plan?.id ?? head.planId,
           produceVersionId: head.produceVersionId,
@@ -440,10 +564,29 @@
         };
       },
 
+      async handleCache() {
+        const payload = this.buildSavePayload(1);
+        const loading = this.$loading({ lock: true, text: '缓存中...' });
+        try {
+          await savePlanDotLine(payload);
+          this.$emit('save', {
+            plan: this.currentPlan,
+            taskList: this.taskList,
+            payload
+          });
+          this.$message.success('缓存成功');
+          this.onModalClose();
+        } catch (e) {
+          this.$message.error(e.message || '缓存失败');
+        } finally {
+          loading.close();
+        }
+      },
+
       async handleSave() {
         if (!this.validateTaskList()) return;
 
-        const payload = this.buildSavePayload();
+        const payload = this.buildSavePayload(0);
         const loading = this.$loading({ lock: true, text: '保存中...' });
         try {
           await savePlanDotLine(payload);
@@ -462,11 +605,14 @@
       },
 
       onModalClose() {
+        window.removeEventListener('resize', this.calcTableHeight);
+        this.modalFullscreen = false;
         this.visible = false;
         this.dialogVisible = false;
         this.taskList = [];
         this.currentPlan = null;
         this.planRoutingPayload = null;
+        this.routeErrorIndex = -1;
         this.$emit('success');
       }
     }
@@ -476,8 +622,7 @@
 <style lang="scss" scoped>
   .plan-dot-line,
   .top-route,
-  .config-panel,
-  .route-line {
+  .config-panel {
     width: 100%;
     max-width: 100%;
     min-width: 0;
@@ -496,6 +641,11 @@
     background: #fff;
   }
 
+  /* 工艺路线区域随内容增高,不要出现纵向滚动条 */
+  .top-route {
+    overflow: visible;
+  }
+
   .config-panel {
     margin-top: 12px;
   }
@@ -506,53 +656,51 @@
     margin-bottom: 10px;
   }
 
-  .route-line {
-    display: flex;
-    flex-wrap: nowrap;
-    align-items: stretch;
-    color: #303133;
-  }
-
-  .route-segment {
-    flex: 1 1 0;
-    min-width: 0;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-
-  .route-node {
-    flex: 0 1 auto;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    min-width: 0;
-    min-height: 22px;
-    border-radius: 4px;
-    padding: 2px 10px;
-    color: #fff;
-    background: #909399;
-    font-size: clamp(10px, 2.6vw, 12px);
-    font-weight: 600;
-    line-height: 1.2;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-    text-align: center;
-  }
-
-  .route-node--done {
-    background: #56bf1d;
+  .route-steps {
+    width: 100%;
+    overflow-x: auto;
+    overflow-y: hidden;
+    padding-bottom: 8px;
+    box-sizing: content-box;
   }
 
-  .route-arrow {
-    flex: 1 1 0;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    color: #909399;
-    font-size: clamp(14px, 2.5vw, 18px);
-    line-height: 1;
+  .route-steps ::v-deep {
+    .el-steps {
+      display: flex;
+      flex-wrap: nowrap;
+      align-items: flex-start;
+    }
+    .el-step {
+      flex-shrink: 0;
+    }
+    .el-step__title {
+      font-size: clamp(16px, 2.2vw, 16px);
+      line-height: 1.35;
+      max-width: 8em;
+      margin-left: auto;
+      margin-right: auto;
+      white-space: normal;
+      word-break: break-all;
+    }
+    .el-step.active .el-step__title {
+      color: #409eff;
+      font-weight: 600;
+    }
+    .el-step__line {
+      top: 14px;
+    }
+    .el-step__icon {
+      width: 30px;
+      height: 30px;
+    }
+    .el-step__main {
+      padding-top: 8px;
+      margin-top: 0;
+    }
+    .el-step__description {
+      margin-top: 0;
+      padding-right: 0;
+    }
   }
 
   @media (max-width: 576px) {
@@ -560,12 +708,6 @@
     .config-panel {
       padding: 8px;
     }
-    .route-node {
-      padding: 2px 4px;
-    }
-    .route-arrow {
-      font-size: 12px;
-    }
   }
 
   .task-config-table-wrap {
@@ -573,7 +715,6 @@
   }
 
   .config-table ::v-deep {
-    .el-table th > .cell,
     .el-table td > .cell {
       font-size: 14px;
       text-align: center;
@@ -595,3 +736,22 @@
     }
   }
 </style>
+
+<!-- 弹窗 append-to-body 后表头不在带 scoped 的节点链上,需用弹窗 class 单独写表头 -->
+<style lang="scss">
+  .plan-dot-line-dialog {
+    .task-config-table-wrap
+      .el-table__header-wrapper
+      th.el-table__cell
+      > .cell {
+      font-size: 15px !important;
+      font-weight: 600 !important;
+      line-height: 1.4 !important;
+    }
+
+    .top-route {
+      overflow: visible !important;
+      max-height: none !important;
+    }
+  }
+</style>

+ 6 - 3
src/views/productionPlan/index.vue

@@ -374,10 +374,10 @@
           <el-link
             type="primary"
             :underline="false"
-            v-if="planDotLineEnabled"
+            v-if="planDotLineEnabled && (row.status == 3 || row.status == 2)"
             @click="planDotLine(row)"
           >
-            计划布点
+            布点
           </el-link>
 
           <el-link
@@ -385,7 +385,7 @@
             :underline="false"
             @click="productionPreparations(row)"
           >
-            生产准备
+            生产准备
           </el-link>
         </template>
       </ele-pro-table>
@@ -1332,6 +1332,9 @@
       },
 
       planDotLine(row) {
+        if (!row.bomCategoryId) {
+          return this.$message.warning('该计划没有定义BOM,请先添加BOM!');
+        }
         this.$refs.planDotLineRef.open(row);
       },
 

+ 115 - 69
src/views/workOrder/components/details.vue

@@ -374,7 +374,11 @@
           </el-form>
         </el-tab-pane>
 
-        <el-tab-pane label="布点详情" name="dotLine" v-if="hasDotLineDetail">
+        <el-tab-pane
+          label="计划布点详情"
+          name="dotLine"
+          v-if="hasDotLineDetail"
+        >
           <div class="plan-dot-line">
             <div class="top-route">
               <div class="panel-title">工艺路线</div>
@@ -382,26 +386,21 @@
                 v-if="dotLineTaskList.length === 0"
                 description="暂无工艺路线"
               ></el-empty>
-              <div v-else class="route-line">
-                <div
+              <el-steps
+                v-else
+                :active="dotLineRouteStepsActive"
+                space="20px"
+                align-center
+                finish-status="success"
+                class="route-steps"
+              >
+                <el-step
                   v-for="(item, index) in dotLineTaskList"
-                  :key="`route-seg-${item._taskKey}`"
-                  class="route-segment"
-                >
-                  <span
-                    class="route-node"
-                    :class="{ 'route-node--done': isDotLineTaskDone(item) }"
-                  >
-                    {{ getDotLineTaskName(item, index) }}
-                  </span>
-                  <span
-                    v-if="index < dotLineTaskList.length - 1"
-                    class="route-arrow"
-                    aria-hidden="true"
-                    >→</span
-                  >
-                </div>
-              </div>
+                  :key="`route-step-${item._taskKey}`"
+                  :title="dotLineRouteStepTitle(item, index)"
+                  :class="{ active: dotLineRouteDesIndex === index }"
+                ></el-step>
+              </el-steps>
             </div>
             <div class="config-panel">
               <div class="panel-title">工艺配置</div>
@@ -421,14 +420,17 @@
                   <el-table-column
                     type="index"
                     label="序号"
-                    width="48"
+                    width="60"
                     align="center"
+                    class-name="process-name-cell"
+                    label-class-name="config-execution-team-header"
                   />
                   <el-table-column
                     label="工序名称"
                     min-width="100"
                     show-overflow-tooltip
-                    class-name="task-name-cell"
+                    class-name="process-name-cell"
+                    label-class-name="config-execution-team-header"
                     align="center"
                   >
                     <template slot-scope="{ row, $index }">
@@ -441,6 +443,8 @@
                     label="执行模式"
                     min-width="100"
                     align="center"
+                    label-class-name="config-execution-team-header"
+                    class-name="config-meta-cell"
                   >
                     <template slot-scope="{ row }">
                       {{ executionTypeLabel(row.executionType) }}
@@ -451,18 +455,24 @@
                     min-width="130"
                     align="center"
                     prop="executionTeamName"
+                    label-class-name="config-execution-team-header"
+                    class-name="config-meta-cell"
                   />
                   <el-table-column
                     label="执行开始时间"
                     min-width="168"
                     align="center"
                     prop="executionStartTime"
+                    label-class-name="config-execution-team-header"
+                    class-name="config-meta-cell"
                   />
                   <el-table-column
                     label="执行结束时间"
                     min-width="168"
                     align="center"
                     prop="executionEndTime"
+                    label-class-name="config-execution-team-header"
+                    class-name="config-meta-cell"
                   />
                 </el-table>
               </div>
@@ -639,6 +649,21 @@
       executionTypeLabel() {
         return (val) => EXEC_TYPE_MAP[val] ?? '';
       },
+      /** 与计划布点弹窗一致:首个未配置执行模式的工序为当前步;全部已配置则全部 finish */
+      dotLineRouteStepsActive() {
+        const list = this.dotLineTaskList;
+        if (!list.length) return 0;
+        for (let i = 0; i < list.length; i++) {
+          if (!this.isDotLineTaskDone(list[i])) return i;
+        }
+        return list.length;
+      },
+      dotLineRouteDesIndex() {
+        const list = this.dotLineTaskList;
+        if (!list.length) return -1;
+        const a = this.dotLineRouteStepsActive;
+        return a >= list.length ? list.length - 1 : a;
+      },
       columns() {
         return [
           {
@@ -815,6 +840,12 @@
         return item.taskName || item.name || `工艺${index + 1}`;
       },
 
+      dotLineRouteStepTitle(item, index) {
+        return (
+          item.taskTypeName || item.taskName || item.name || `工艺${index + 1}`
+        );
+      },
+
       isDotLineTaskDone(item) {
         return item.executionType != null && item.executionType !== '';
       },
@@ -861,8 +892,7 @@
 
   .plan-dot-line,
   .top-route,
-  .config-panel,
-  .route-line {
+  .config-panel {
     width: 100%;
     max-width: 100%;
     min-width: 0;
@@ -870,7 +900,7 @@
   }
 
   .plan-dot-line {
-    min-height: 200px;
+    min-height: 360px;
   }
 
   .top-route,
@@ -881,6 +911,10 @@
     background: #fff;
   }
 
+  .top-route {
+    overflow: visible;
+  }
+
   .config-panel {
     margin-top: 12px;
   }
@@ -891,53 +925,58 @@
     margin-bottom: 10px;
   }
 
-  .route-line {
-    display: flex;
-    flex-wrap: nowrap;
-    align-items: stretch;
-    color: #303133;
-  }
-
-  .route-segment {
-    flex: 1 1 0;
-    min-width: 0;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-
-  .route-node {
-    flex: 0 1 auto;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    min-width: 0;
-    min-height: 22px;
-    border-radius: 4px;
-    padding: 2px 10px;
-    color: #fff;
-    background: #909399;
-    font-size: clamp(10px, 2.6vw, 12px);
-    font-weight: 600;
-    line-height: 1.2;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-    text-align: center;
+  .route-steps {
+    width: 100%;
+    overflow-x: auto;
+    overflow-y: hidden;
+    padding-bottom: 8px;
+    box-sizing: content-box;
   }
 
-  .route-node--done {
-    background: #56bf1d;
+  .route-steps ::v-deep {
+    .el-steps {
+      display: flex;
+      flex-wrap: nowrap;
+      align-items: flex-start;
+    }
+    .el-step {
+      flex-shrink: 0;
+    }
+    .el-step__title {
+      font-size: clamp(16px, 2.2vw, 16px);
+      line-height: 1.35;
+      max-width: 8em;
+      margin-left: auto;
+      margin-right: auto;
+      white-space: normal;
+      word-break: break-all;
+    }
+    .el-step.active .el-step__title {
+      color: #409eff;
+      font-weight: 600;
+    }
+    .el-step__line {
+      top: 14px;
+    }
+    .el-step__icon {
+      width: 30px;
+      height: 30px;
+    }
+    .el-step__main {
+      padding-top: 8px;
+      margin-top: 0;
+    }
+    .el-step__description {
+      margin-top: 0;
+      padding-right: 0;
+    }
   }
 
-  .route-arrow {
-    flex: 1 1 0;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    color: #909399;
-    font-size: clamp(14px, 2.5vw, 18px);
-    line-height: 1;
+  @media (max-width: 576px) {
+    .top-route,
+    .config-panel {
+      padding: 8px;
+    }
   }
 
   .task-config-table-wrap {
@@ -950,11 +989,18 @@
       font-size: 14px;
       text-align: center;
     }
+    th.config-execution-team-header > .cell,
+    td.config-meta-cell > .cell {
+      font-size: 14px;
+      font-weight: 500;
+      line-height: 1.4;
+    }
     .el-table__body .cell {
       padding-left: 6px;
       padding-right: 6px;
     }
-    td.task-name-cell .task-name-text {
+    th.process-name-header > .cell,
+    td.process-name-cell .task-name-text {
       font-size: 14px;
       font-weight: 500;
       line-height: 1.4;