Ver Fonte

feat: 生产工单详情抽屉新增作业操作 Tab(首件两检/产前准备/过程控制/产后检查/任务/委外请托/异常)并放开布点详情

详情抽屉原本只有只读列表,作业操作入口都在 produce 作业台(footBtn + workReport)。本次把两者打通:

1. qualityInspection / taskDialog / createError 三个弹窗组件新增 inline 模式,用 <component :is> 换掉弹窗外壳,面板本体不动,produce 作业台行为不变

2. 新增 details/tabs 下 4 个包装组件(firstArticleTab/taskTab/errorTab/outsourceTab),内联渲染上述面板,委外请托含 pcCheckOutsource 工序可委外校验

3. recordRules 新增 fixedReportWorkType 属性,传入 1/2/3 时隐藏内部类型切换下拉,产前准备/过程控制/产后检查三个 Tab 复用同一份记录规则数据,按类型隔离 cacheKey

4. taskDialog 的 open() 支持显式传 produceTaskId,解掉对 store.taskObj.id 的硬依赖(详情抽屉不写 store);并修复缺参数时 loading 不复位导致表格一直转圈

5. 放开布点详情 Tab,加 lazy 与 productionPlanId 判断,避免空 Tab 和开抽屉即发请求

6. Tab 显隐沿用 footBtn 的 isShow 权限码;工序切换时 tabsRenderKey 自增强制重挂载,操作面板跟随当前工序取数

7. 修复首件两检 inline 下 pane 高度跳动:Tab 面板父级高度为 auto,height:100% 无法解析导致 el-table 退回按内容实测高度并在数据返回后重排,改为 max-height:68vh 按内容自适应

8. 关闭/打开抽屉时重置 detailActiveTab 为第一个 Tab(领料详情),open() 兜底父组件直接置 visible=false 不触发 before-close 的路径

9. getTaskFn 的 desIndex 加 findIndex 未命中兜底(-1 会导致取不到工序数据,新增 Tab 全部为空)
xieyong há 2 semanas atrás
pai
commit
ef20e3a473

+ 68 - 10
src/views/produce/components/createError.vue

@@ -1,10 +1,14 @@
 <template>
-  <ele-modal
-    :visible.sync="visible"
-    v-if="visible"
-    :title="title"
-    width="45vw"
-    append-to-body
+  <!--
+    inline=true 时不套弹窗,直接把新增异常表单渲染到父级(详情抽屉的「异常」Tab)。
+    注意:外壳用 v-if="visible || inline",inline 模式下 open() 已把 visible 置 true,
+    且 cancel() 不再置 false,表单常驻。
+  -->
+  <component
+    :is="inline ? 'div' : 'ele-modal'"
+    v-if="visible || inline"
+    v-bind="shellProps"
+    :class="{ 'create-error-inline': inline }"
     @close="cancel"
   >
     <div class="switch" v-if="type == 'detail'" :maxable="true">
@@ -245,11 +249,23 @@
         确定
       </el-button>
     </template>
+    <!-- inline 模式没有弹窗底栏,表单底部自带操作区 -->
+    <div v-if="inline" class="create-error-actions">
+      <el-button @click="reset">重置</el-button>
+      <el-button
+        type="primary"
+        @click="save"
+        :loading="loading"
+        v-if="type != 'detail'"
+      >
+        确定
+      </el-button>
+    </div>
     <detail
       v-if="activeComp === 'bpm' && form.processInstanceId"
       :id="form.processInstanceId"
     ></detail>
-  </ele-modal>
+  </component>
 </template>
 
 <script>
@@ -285,6 +301,16 @@
   };
   export default {
     components: { fileUpload, detail },
+    props: {
+      /**
+       * true:不弹窗,新增异常表单直接渲染在父容器内(生产工单详情抽屉的「异常」Tab)
+       * false:保持原有 ele-modal 弹窗行为(produce 作业台)
+       */
+      inline: {
+        type: Boolean,
+        default: false
+      }
+    },
     data() {
       return {
         visible: false,
@@ -323,6 +349,18 @@
     },
 
     computed: {
+      // 弹窗模式下的弹窗参数;inline 模式为空对象(外壳退化为 div)
+      shellProps() {
+        if (this.inline) {
+          return {};
+        }
+        return {
+          visible: this.visible,
+          title: this.title,
+          width: '45vw',
+          appendToBody: true
+        };
+      },
       // 是否开启响应式布局
       styleResponsive() {
         return this.$store.state.theme.styleResponsive;
@@ -363,9 +401,18 @@
         }
       },
       cancel() {
-        this.form = { ...defaultForm() };
+        this.reset();
+        // inline 模式下表单常驻在 Tab 里,只清空不隐藏
+        if (this.inline) {
+          return;
+        }
         this.visible = false;
       },
+      // 清空表单到初始态
+      reset() {
+        this.form = { ...defaultForm() };
+        this.activeComp = 'main';
+      },
       choose(data) {
         this.form.relationJson = JSON.parse(
           JSON.stringify(transitionData(data, this.form.relationType))
@@ -396,8 +443,12 @@
             .then((res) => {
               this.loading = false;
               this.$message.success('成功');
-              // this.cancel();
-              // this.$emit('refresh');
+              this.$emit('refresh');
+              // inline 模式下重置表单继续录入,不关闭 Tab
+              if (this.inline) {
+                this.reset();
+                return;
+              }
               this.visible = false;
             })
             .catch((e) => {
@@ -427,4 +478,11 @@
   .create-form .el-form-item {
     margin-bottom: 15px !important;
   }
+
+  .create-error-actions {
+    display: flex;
+    justify-content: center;
+    padding: 10px 0 4px;
+    border-top: 1px solid #ebeef5;
+  }
 </style>

+ 147 - 91
src/views/produce/components/qualityInspection/index.vue

@@ -1,15 +1,14 @@
 <template>
-  <ele-modal
-    title="首件两检报工"
-    :visible.sync="dialogVisible"
-    width="95%"
-    :before-close="handleClose"
-    append-to-body
-    :maxable="true"
-    :close-on-click-modal="false"
-    :close-on-press-escape="false"
+  <!-- inline=true 时不套弹窗,直接把面板渲染到父级(详情抽屉的 Tab 内) -->
+  <component
+    :is="inline ? 'div' : 'ele-modal'"
+    v-bind="shellProps"
+    :class="{ 'quality-inspection-inline': inline }"
   >
-    <div class="two-layout quality-inspection-layout">
+    <div
+      class="two-layout quality-inspection-layout"
+      :class="{ 'is-inline': inline }"
+    >
       <ele-split-layout
         class="quality-inspection-split"
         space="12px"
@@ -26,89 +25,90 @@
         :responsive="false"
       >
         <div class="pane pane-left">
-            <div class="pane-title">首件两检</div>
-            <div
-              v-show="leftMode === 'list'"
-              class="fill-box"
-              v-loading="loading"
+          <div class="pane-title">首件两检</div>
+          <div
+            v-show="leftMode === 'list'"
+            class="fill-box"
+            v-loading="loading"
+          >
+            <ele-pro-table
+              ref="table"
+              row-key="id"
+              :columns="columns"
+              :datasource="list"
+              :cache-key="tableCacheKey"
+              autoAmendPage
+              :need-page="true"
+              @refresh="getData"
+              :height="tableHeight"
+              :max-height="tableMaxHeight"
             >
-              <ele-pro-table
-                ref="table"
-                row-key="id"
-                :columns="columns"
-                :datasource="list"
-                cache-key="mes-ruleRecordsList-20260208-1"
-                autoAmendPage
-                :need-page="true"
-                @refresh="getData"
-                height="calc(80vh - 230px)"
+              <template v-slot:selfCheck="{ row }">
+                <el-button
+                  type="primary"
+                  @click="selfRequest(row)"
+                  v-if="row.status == 0"
+                  >自检请托</el-button
+                >
+                <el-button
+                  type="primary"
+                  :disabled="row.status != 0"
+                  @click="reportClick(row, 1)"
+                  >{{ row.status == 0 ? '报工' : '已报工' }}</el-button
+                >
+
+                <el-button
+                  type="primary"
+                  v-if="row.status == 2 || row.status == 3"
+                  @click="reportClick(row, 1, 'detail')"
+                  >详情</el-button
+                >
+              </template>
+
+              <template
+                v-slot:specialInspection="{ row }"
+                v-if="
+                  $hasPermission(
+                    'mes:firstarticledualinspectionitem:specialreport'
+                  )
+                "
               >
-                <template v-slot:selfCheck="{ row }">
-                  <el-button
-                    type="primary"
-                    @click="selfRequest(row)"
-                    v-if="row.status == 0"
-                    >自检请托</el-button
-                  >
-                  <el-button
-                    type="primary"
-                    :disabled="row.status != 0"
-                    @click="reportClick(row, 1)"
-                    >{{ row.status == 0 ? '报工' : '已报工' }}</el-button
-                  >
-
-                  <el-button
-                    type="primary"
-                    v-if="row.status == 2 || row.status == 3"
-                    @click="reportClick(row, 1, 'detail')"
-                    >详情</el-button
-                  >
-                </template>
-
-                <template
-                  v-slot:specialInspection="{ row }"
-                  v-if="
-                    $hasPermission(
-                      'mes:firstarticledualinspectionitem:specialreport'
-                    )
-                  "
+                <el-button
+                  type="primary"
+                  :disabled="row.status != 2"
+                  @click="reportClick(row, 2)"
+                  >{{ row.status == 3 ? '已报工' : '报工' }}</el-button
                 >
-                  <el-button
-                    type="primary"
-                    :disabled="row.status != 2"
-                    @click="reportClick(row, 2)"
-                    >{{ row.status == 3 ? '已报工' : '报工' }}</el-button
-                  >
-
-                  <el-button
-                    type="primary"
-                    v-if="row.status == 3"
-                    @click="reportClick(row, 2, 'detail')"
-                    >详情</el-button
-                  >
-                </template>
-
-                <template v-slot:status="{ row }">
-                  <el-tag v-if="row.status == 0">待自检</el-tag>
-                  <el-tag v-if="row.status == 2">待专检</el-tag>
-                  <el-tag v-if="row.status == 3">已完成</el-tag>
-                </template>
-              </ele-pro-table>
-            </div>
 
-            <self-inspection-reporting
-              v-if="leftMode === 'report'"
-              ref="selfReportingRef"
-              @cancel="backToList"
-              @success="reportSuccess"
-            />
-
-            <self-inspection-request
-              v-if="leftMode === 'request'"
-              ref="selfRequestRef"
-              @cancel="backToList"
-              @success="reportSuccess"
-            />
+                <el-button
+                  type="primary"
+                  v-if="row.status == 3"
+                  @click="reportClick(row, 2, 'detail')"
+                  >详情</el-button
+                >
+              </template>
+
+              <template v-slot:status="{ row }">
+                <el-tag v-if="row.status == 0">待自检</el-tag>
+                <el-tag v-if="row.status == 2">待专检</el-tag>
+                <el-tag v-if="row.status == 3">已完成</el-tag>
+              </template>
+            </ele-pro-table>
+          </div>
+
+          <self-inspection-reporting
+            v-if="leftMode === 'report'"
+            ref="selfReportingRef"
+            @cancel="backToList"
+            @success="reportSuccess"
+          />
+
+          <self-inspection-request
+            v-if="leftMode === 'request'"
+            ref="selfRequestRef"
+            @cancel="backToList"
+            @success="reportSuccess"
+          />
         </div>
 
         <template v-slot:content>
@@ -131,7 +131,7 @@
       ref="selfRequestRef"
       @refreshData="getData"
     ></self-inspection-request> -->
-  </ele-modal>
+  </component>
 </template>
 
 <script>
@@ -150,6 +150,16 @@
       selfInspectionRequest,
       wokePopup
     },
+    props: {
+      /**
+       * true:不弹窗,面板直接渲染在父容器内(生产工单详情抽屉的「首件两检」Tab)
+       * false:保持原有 ele-modal 弹窗行为(produce 作业台)
+       */
+      inline: {
+        type: Boolean,
+        default: false
+      }
+    },
     data() {
       return {
         dialogVisible: false,
@@ -165,6 +175,42 @@
       };
     },
     computed: {
+      // 弹窗模式下的弹窗参数;inline 模式为空对象(外壳退化为 div)
+      shellProps() {
+        if (this.inline) {
+          return {};
+        }
+        return {
+          title: '首件两检报工',
+          visible: this.dialogVisible,
+          width: '95%',
+          appendToBody: true,
+          maxable: true,
+          closeOnClickModal: false,
+          closeOnPressEscape: false,
+          beforeClose: this.handleClose
+        };
+      },
+      /**
+       * 表格高度。
+       * 弹窗模式:弹窗外壳是 80vh 的确定高度,用固定高度即可。
+       * inline 模式:Tab 面板父容器高度是 auto,百分比高度无法解析,
+       * el-table 会退回按内容实测高度并在每次数据变化后重排(表现为 pane 高度跳动),
+       * 因此这里不传 height,改用 maxHeight 按内容自适应 + 视口封顶。
+       */
+      tableHeight() {
+        return this.inline ? undefined : 'calc(80vh - 230px)';
+      },
+      // 与本抽屉其它详情 tab 一致:max-height: 68vh
+      tableMaxHeight() {
+        return this.inline ? '68vh' : undefined;
+      },
+      // 列缓存按实例隔离,避免同一缓存键在多个 Tab/弹窗之间串配置
+      tableCacheKey() {
+        return this.inline
+          ? 'mes-qualityInspection-inline-20260208-1'
+          : 'mes-ruleRecordsList-20260208-1';
+      },
       columns() {
         return [
           {
@@ -407,6 +453,13 @@
     background: #f5f7fa;
   }
 
+  // inline(生产工单详情抽屉 Tab 内)模式:高度交给内容决定。
+  // 不要再写 height: 100% —— Tab 面板父级是 auto,百分比解析不出来,
+  // el-table 只能按内容实测高度并在数据返回后重排,导致左侧 pane 高度跳动。
+  .two-layout.is-inline {
+    height: auto;
+  }
+
   .quality-inspection-split {
     flex: 1;
     height: 100%;
@@ -414,7 +467,10 @@
   }
 
   .quality-inspection-layout ::v-deep .ele-split-layout,
-  .quality-inspection-layout ::v-deep .ele-split-layout > .ele-split-layout-content {
+  .quality-inspection-layout
+    ::v-deep
+    .ele-split-layout
+    > .ele-split-layout-content {
     height: 100%;
     min-height: 0;
   }

+ 63 - 19
src/views/produce/components/taskDialog/index.vue

@@ -1,13 +1,9 @@
 <template>
-  <ele-modal
-    :centered="true"
-    :close-on-click-modal="false"
-    :maxable="true"
-    :visible.sync="visible"
-    append-to-body
-    custom-class="ele-dialog-form long-dialog-form"
-    title="任务列表"
-    width="85%"
+  <!-- inline=true 时不套弹窗,直接把任务列表渲染到父级(详情抽屉的 Tab 内) -->
+  <component
+    :is="inline ? 'div' : 'ele-modal'"
+    v-bind="shellProps"
+    :class="{ 'task-dialog-inline': inline }"
     @close="handleClose"
   >
     <ele-pro-table
@@ -15,7 +11,7 @@
       :columns="columns"
       :datasource="workList"
       :loading="loading"
-      style="min-height: 400px"
+      :style="tableStyle"
     >
       <template v-slot:realStartTime="{ row }">
         <el-date-picker
@@ -137,7 +133,7 @@
       </template>
     </ele-pro-table>
     <modifyDialog ref="modifyRef" />
-  </ele-modal>
+  </component>
 </template>
 
 <script>
@@ -153,6 +149,16 @@
   export default {
     // components: { WorkTimeHistory, Details },
     components: { modifyDialog },
+    props: {
+      /**
+       * true:不弹窗,任务列表直接渲染在父容器内(生产工单详情抽屉的「任务」Tab)
+       * false:保持原有 ele-modal 弹窗行为(produce 作业台)
+       */
+      inline: {
+        type: Boolean,
+        default: false
+      }
+    },
     data() {
       return {
         visible: false,
@@ -297,13 +303,49 @@
         loading: false
       };
     },
-    computed: {},
+    computed: {
+      // 弹窗模式下的弹窗参数;inline 模式为空对象(外壳退化为 div)
+      shellProps() {
+        if (this.inline) {
+          return {};
+        }
+        return {
+          centered: true,
+          visible: this.visible,
+          appendToBody: true,
+          customClass: 'ele-dialog-form long-dialog-form',
+          title: '任务列表',
+          width: '85%',
+          maxable: true,
+          closeOnClickModal: false
+        };
+      },
+      /**
+       * 表格外层高度。
+       * 两种模式都不传 el-table 的 height prop,表格按内容自适应,
+       * 只给一个最小高度兜底 —— Tab 面板父级是 auto,百分比高度解析不出来,
+       * 交给 el-table 去实测反而会引发重排。
+       */
+      tableStyle() {
+        return { minHeight: this.inline ? '420px' : '400px' };
+      }
+    },
     methods: {
-      open(queryParam, workData) {
+      /**
+       * @param {Object} queryParam 工序上下文(workStepQueryParam)
+       * @param {Object} workData   工单信息
+       * @param {Object} [options]  { produceTaskId } 显式指定工序 id。
+       *        不传时回退到 store 里的 taskObj.id(produce 作业台的原有行为)。
+       *        详情抽屉未向 store 写入 taskObj,必须显式传入。
+       */
+      open(queryParam, workData, options = {}) {
         this.visible = true;
         this.params = {
           produceRoutingId: workData.produceRoutingId,
-          taskId: this.$store.state.user.taskObj.id,
+          taskId:
+            options.produceTaskId ||
+            (queryParam && queryParam.produceTaskId) ||
+            (this.$store.state.user.taskObj || {}).id,
           mesWorkOrderId: workData.id,
           categoryId: workData.categoryId,
           apsWorkOrderId: workData.apsWorkOrderId
@@ -333,12 +375,14 @@
       },
       getData() {
         this.workList = [];
-        this.loading = true;
-        for (let key in this.params) {
-          if (!this.params[key]) {
-            return;
-          }
+        // 缺参数时直接结束并复位 loading,避免表格一直转圈
+        const params = this.params || {};
+        const missing = Object.keys(params).some((key) => !params[key]);
+        if (missing) {
+          this.loading = false;
+          return;
         }
+        this.loading = true;
         pageAssigneeByTask(this.params)
           .then((res) => {
             this.loading = false;

+ 169 - 11
src/views/produceOrder/components/details/index.vue

@@ -31,10 +31,7 @@
         <!-- //详细信息 -->
         <Info :workOrderInfo="workOrderInfo"></Info>
 
-        <div
-          class="route-flow"
-          :style="{ height: stepsRowHeight + 'px' }"
-        >
+        <div class="route-flow" :style="{ height: stepsRowHeight + 'px' }">
           <div
             v-for="(col, colIndex) in stepColumns"
             :key="`route-unit-${colIndex}`"
@@ -148,9 +145,124 @@
               :workOrderInfo="workOrderInfo"
             ></checkList>
           </el-tab-pane>
-          <!-- <el-tab-pane label="布点详情" name="dot">
+          <!--
+            布点详情:计划级数据(按 productionPlanId 取),不随顶部步骤条切换的工序变化。
+            lazy 避免开抽屉就发请求;无生产计划的工单不展示,避免常驻空 tab。
+          -->
+          <el-tab-pane
+            label="布点详情"
+            name="dot"
+            lazy
+            v-if="hasProductionPlan"
+          >
             <dotLineDetail :workOrderInfo="workOrderInfo"></dotLineDetail>
-          </el-tab-pane> -->
+          </el-tab-pane>
+
+          <!-- ===== 作业操作区:以下 Tab 复用 produce 作业台(footBtn/workReport)的操作面板 ===== -->
+
+          <!-- 首件两检 -->
+          <el-tab-pane
+            label="首件两检"
+            name="qualityInspection"
+            lazy
+            v-if="$hasPermission('mes:firstarticledualinspection')"
+          >
+            <firstArticleTab
+              :workOrderInfo="workOrderInfo"
+              :produceTaskId="sourceTaskId"
+              :taskInstanceId="taskInstanceId"
+            ></firstArticleTab>
+          </el-tab-pane>
+
+          <!-- 产前准备 / 过程控制 / 产后检查:同一份记录规则事项数据,reportWorkType 分别为 1/2/3 -->
+          <el-tab-pane
+            label="产前准备"
+            name="prenatalExamination"
+            lazy
+            v-if="
+              $hasPermission('mes:producetaskrulerecord:save1') && hasRuleRecord
+            "
+          >
+            <recordRules
+              :produceTaskId="sourceTaskId"
+              :workOrderInfo="workOrderInfo"
+              :fixedReportWorkType="1"
+            ></recordRules>
+          </el-tab-pane>
+
+          <el-tab-pane
+            label="过程控制"
+            name="processDetection"
+            lazy
+            v-if="
+              $hasPermission('mes:producetaskrulerecord:save2') && hasRuleRecord
+            "
+          >
+            <recordRules
+              :produceTaskId="sourceTaskId"
+              :workOrderInfo="workOrderInfo"
+              :fixedReportWorkType="2"
+            ></recordRules>
+          </el-tab-pane>
+
+          <el-tab-pane
+            label="产后检查"
+            name="postpartumExamination"
+            lazy
+            v-if="
+              $hasPermission('mes:producetaskrulerecord:save3') && hasRuleRecord
+            "
+          >
+            <recordRules
+              :produceTaskId="sourceTaskId"
+              :workOrderInfo="workOrderInfo"
+              :fixedReportWorkType="3"
+            ></recordRules>
+          </el-tab-pane>
+
+          <!-- 任务 -->
+          <el-tab-pane
+            label="任务"
+            name="task"
+            lazy
+            v-if="$hasPermission('aps:assign:save')"
+          >
+            <taskTab
+              :workOrderInfo="workOrderInfo"
+              :produceTaskId="sourceTaskId"
+              :taskInstanceId="taskInstanceId"
+            ></taskTab>
+          </el-tab-pane>
+
+          <!-- 委外请托 -->
+          <el-tab-pane
+            label="委外请托"
+            name="outsourcing"
+            lazy
+            v-if="$hasPermission('mes:applyoutsource:save')"
+          >
+            <outsourceTab
+              :workOrderInfo="workOrderInfo"
+              :produceTaskId="sourceTaskId"
+              :taskInstanceId="taskInstanceId"
+              :taskObj="curRouteItem"
+              @refresh="reloadDetail"
+            ></outsourceTab>
+          </el-tab-pane>
+
+          <!-- 异常 -->
+          <el-tab-pane
+            label="异常"
+            name="error"
+            lazy
+            v-if="$hasPermission('mes:anomaly:save')"
+          >
+            <errorTab
+              :workOrderInfo="workOrderInfo"
+              :produceTaskId="sourceTaskId"
+              @refresh="reloadDetail"
+            ></errorTab>
+          </el-tab-pane>
         </el-tabs>
       </div>
     </el-drawer>
@@ -167,6 +279,10 @@
   import pickDetails from '../../details.vue';
   import recordRules from '../recordRules/recordRules.vue';
   import dotLineDetail from './dotLineDetail.vue';
+  import firstArticleTab from './tabs/firstArticleTab.vue';
+  import taskTab from './tabs/taskTab.vue';
+  import errorTab from './tabs/errorTab.vue';
+  import outsourceTab from './tabs/outsourceTab.vue';
   export default {
     components: {
       Info,
@@ -177,7 +293,11 @@
       recordRules,
       /** 异步加载,避免 checklist → checkDetails → 本组件 的循环依赖导致 checkList 未注册 */
       checkList: () => import('@/views/checklistManagement/checklist.vue'),
-      dotLineDetail
+      dotLineDetail,
+      firstArticleTab,
+      taskTab,
+      errorTab,
+      outsourceTab
     },
     data() {
       return {
@@ -196,6 +316,8 @@
           id: null
         },
         sourceTaskId: '',
+        /** 当前所选工序的实例 id(routeList[desIndex].taskId),首件两检按它取数 */
+        taskInstanceId: '',
         /** 任务数据就绪后递增,强制各 Tab 子组件重挂载以重新拉数 */
         tabsRenderKey: 0,
         detailActiveTab: 'pick',
@@ -206,6 +328,24 @@
     },
 
     computed: {
+      // 步骤条当前所选工序的完整数据,供委外请托等面板复用
+      curRouteItem() {
+        const list = Array.isArray(this.routeList) ? this.routeList : [];
+        return list[this.desIndex] || {};
+      },
+      // 记录规则类 Tab(产前准备/过程控制/产后检查/记录详情)是否可展示:
+      // 需要有效的工序配置 id,-1(入库)/-2(完结) 工序没有记录规则
+      hasRuleRecord() {
+        return !!(
+          this.sourceTaskId &&
+          this.sourceTaskId != '-1' &&
+          this.sourceTaskId != '-2'
+        );
+      },
+      // 布点详情是计划级数据,没有生产计划的工单没有布点可看
+      hasProductionPlan() {
+        return !!(this.workOrderInfo && this.workOrderInfo.productionPlanId);
+      },
       // 按 isParallel 连续段把工序拆分成"列",并行工序同列垂直堆叠展示
       stepColumns() {
         const list = Array.isArray(this.routeList) ? this.routeList : [];
@@ -241,6 +381,8 @@
     methods: {
       handleClose() {
         this.activeIndex = 0;
+        // 关闭时回到第一个 Tab,避免下次打开停在上次的操作页
+        this.detailActiveTab = 'pick';
         this.drawer = false;
       },
 
@@ -291,6 +433,10 @@
       },
 
       open(row) {
+        // 每次打开都回到第一个 Tab。
+        // 这里也重置一次(不只 handleClose):el-drawer 在父组件直接把 visible 置 false 时
+        // 不会触发 before-close,光靠 handleClose 兜不住所有关闭路径。
+        this.detailActiveTab = 'pick';
         this.workOrderInfo = this.deepCopy(row);
         this.routeObj.id = this.workOrderInfo.id;
         console.log(this.workOrderInfo.id, 'workOrderInfo.id');
@@ -299,10 +445,15 @@
         this.drawer = true;
       },
 
+      /** 委外请托/异常等操作成功后刷新工单与工序数据 */
+      reloadDetail() {
+        this.getTaskFn();
+      },
+
       getTaskFn() {
         getTaskInstanceList(this.workOrderInfo.id)
           .then((res) => {
-            this.routeList = res;
+            this.routeList = Array.isArray(res) ? res : [];
 
             console.log(this.routeList, '9999999');
 
@@ -313,11 +464,13 @@
 
             console.log('index', index);
 
-            this.desIndex = index;
+            // 工序未命中时兜底到第一道工序,避免 index === -1 导致取不到数据
+            this.desIndex = index > -1 ? index : 0;
             console.log(this.routeList, '888888');
             this.newId = this.routeList[this.desIndex]?.taskId || '';
             this.sourceTaskId =
               this.routeList[this.desIndex]?.sourceTaskId || '';
+            this.taskInstanceId = this.newId;
 
             if (this.workOrderInfo.taskId != -2) {
               // this.curTaskObj = JSON.parse(JSON.stringify(this.routeObj));
@@ -326,10 +479,12 @@
                 id: this.routeObj.id
               };
             } else {
-              this.curTaskObj = JSON.parse(JSON.stringify(this.routeList[0]));
+              this.curTaskObj = JSON.parse(
+                JSON.stringify(this.routeList[0] || {})
+              );
               // this.desIndex = 0;
             }
-            this.activeIndex = index;
+            this.activeIndex = this.desIndex;
             console.log('this.curTaskObj', this.curTaskObj);
             console.log('this.newId', this.newId);
           })
@@ -366,6 +521,9 @@
           this.desIndex = index;
           this.newId = this.routeList[this.desIndex].taskId || '';
           this.sourceTaskId = this.routeList[this.desIndex]?.sourceTaskId || '';
+          this.taskInstanceId = this.newId;
+          // 切换工序后强制各 Tab 重挂载,操作面板按新工序重新取数
+          this.tabsRenderKey += 1;
         }
       }
     },

+ 58 - 0
src/views/produceOrder/components/details/tabs/errorTab.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="operation-tab">
+    <!--
+      异常:直接复用 produce 作业台的新增异常表单(inline 模式,不套弹窗),
+      并带上工单基本信息与流程详情页签。
+    -->
+    <createError ref="panel" inline @refresh="handleRefresh"></createError>
+  </div>
+</template>
+
+<script>
+  import createError from '@/views/produce/components/createError.vue';
+
+  export default {
+    name: 'errorTab',
+    components: { createError },
+    props: {
+      // 工单信息
+      workOrderInfo: {
+        type: Object,
+        required: true
+      },
+      // 工序配置 id
+      produceTaskId: {
+        type: [String, Number],
+        default: ''
+      }
+    },
+    watch: {
+      workOrderInfo: {
+        deep: true,
+        handler() {
+          this.loadPanel();
+        }
+      }
+    },
+    mounted() {
+      this.loadPanel();
+    },
+    methods: {
+      loadPanel() {
+        if (!this.$refs.panel) {
+          return;
+        }
+        this.$refs.panel.open('add', this.workOrderInfo);
+      },
+      handleRefresh() {
+        this.$emit('refresh');
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  .operation-tab {
+    padding: 0;
+  }
+</style>

+ 99 - 0
src/views/produceOrder/components/details/tabs/firstArticleTab.vue

@@ -0,0 +1,99 @@
+<template>
+  <div class="operation-tab">
+    <!--
+      首件两检:直接复用 produce 作业台的首件两检面板(inline 模式,不套弹窗)。
+      左侧为自检/专检列表与报工操作,右侧为工艺文件。
+    -->
+    <qualityInspection
+      v-if="produceTaskId"
+      ref="panel"
+      inline
+    ></qualityInspection>
+    <el-empty v-else description="暂无工序信息"></el-empty>
+  </div>
+</template>
+
+<script>
+  import qualityInspection from '@/views/produce/components/qualityInspection/index.vue';
+
+  export default {
+    name: 'firstArticleTab',
+    components: { qualityInspection },
+    props: {
+      // 工单信息
+      workOrderInfo: {
+        type: Object,
+        required: true
+      },
+      // 工序配置 id(routeList[i].sourceTaskId),对应接口的 produceTaskId
+      produceTaskId: {
+        type: [String, Number],
+        default: ''
+      },
+      // 工序实例 id(routeList[i].taskId),对应接口的 produceTaskInstanceId
+      taskInstanceId: {
+        type: [String, Number],
+        default: ''
+      }
+    },
+    watch: {
+      workOrderInfo() {
+        this.loadPanel();
+      },
+      produceTaskId() {
+        this.loadPanel();
+      },
+      taskInstanceId() {
+        this.loadPanel();
+      }
+    },
+    mounted() {
+      this.loadPanel();
+    },
+    methods: {
+      getCraftFileProductCode() {
+        const row = this.workOrderInfo || {};
+        return (
+          row.productCode ||
+          row.materialCode ||
+          row.categoryCode ||
+          row.productCategoryCode ||
+          ''
+        );
+      },
+      /**
+       * 首件两检按“顶部步骤条当前所选工序”取数:
+       * workOrderInfo.taskId 是进入抽屉时的定位工序,切换工序后不会变,
+       * 这里用当前工序的实例 id 覆盖,保证 produceTaskInstanceId 跟随步骤条。
+       */
+      loadPanel() {
+        if (!this.produceTaskId || !this.$refs.panel) {
+          return;
+        }
+        const row = this.workOrderInfo || {};
+        const workOrder = {
+          ...row,
+          taskId: this.taskInstanceId
+        };
+        const produceTaskInfo = {
+          ...row,
+          id: this.produceTaskId,
+          sourceTaskId: this.produceTaskId
+        };
+        const req = {
+          taskId: this.produceTaskId,
+          workOrderId: row.id,
+          productCode: this.getCraftFileProductCode()
+        };
+
+        this.$refs.panel.open(workOrder, produceTaskInfo, '', req);
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  .operation-tab {
+    padding: 0;
+  }
+</style>

+ 313 - 0
src/views/produceOrder/components/details/tabs/outsourceTab.vue

@@ -0,0 +1,313 @@
+<template>
+  <div v-loading="checking" class="operation-tab">
+    <div v-if="!produceTaskId || !workOrderInfo || !workOrderInfo.id">
+      <el-empty description="暂无工序信息"></el-empty>
+    </div>
+
+    <div v-else-if="!outsourceAllowed" class="outsource-tip">
+      <el-alert
+        :title="outsourceTipMsg"
+        type="info"
+        :closable="false"
+        show-icon
+      ></el-alert>
+    </div>
+
+    <div v-else class="right_main_box">
+      <!-- 委外 / 请托记录列表 -->
+      <outsourcingDetails
+        ref="outsourcingListRef"
+        v-if="!isStep && !isOutsource && !isPleaseEntrust"
+        :chooseType="chooseType"
+        @outsourcingAdd="outsourcingAdd"
+        @pleaseEntrustReceived="handlePleaseEntrustReceived"
+      ></outsourcingDetails>
+
+      <!-- 新增委外(选完工序) -->
+      <outsourcing
+        :outsourceFormVal="outsourceForm"
+        :taskObj="mergedTaskObj"
+        :workListIds="workOrderInfo.id"
+        :workData="workInfoList"
+        :chooseType="chooseType"
+        @changePlugIn="changePlugIn"
+        @changeCancel="changeCancel"
+        v-if="isStep"
+      ></outsourcing>
+
+      <!-- 委外单列表 -->
+      <outsourceList
+        :workData="workInfoList"
+        :taskObj="mergedTaskObj"
+        :outsourceFormVal="outsourceForm"
+        :chooseType="chooseType"
+        @closeForm="closeForm"
+        @outScucc="outScucc"
+        v-if="isOutsource"
+      ></outsourceList>
+
+      <!-- 请托单列表 -->
+      <pleaseEntrust
+        :outsourceFormVal="outObj"
+        @closeForm="closeForm"
+        @outScucc="outScucc"
+        v-if="isPleaseEntrust"
+      ></pleaseEntrust>
+    </div>
+
+    <addPlease ref="addPleaseRef" @refresh="refreshPlease"></addPlease>
+  </div>
+</template>
+
+<script>
+  import outsourcingDetails from '@/views/produce/components/outsourcing/details.vue';
+  import outsourcing from '@/views/produce/components/outsourcing/index.vue';
+  import outsourceList from '@/views/produce/components/outsourcing/new_outsourceList.vue';
+  import pleaseEntrust from '@/views/produce/components/outsourcing/pleaseEntrust.vue';
+  import addPlease from '@/views/produce/components/outsourcing/addPlease.vue';
+  import {
+    pcCheckOutsource,
+    getTaskInstanceById,
+    checkOutsourceMaterial,
+    checkPleaseEntrust
+  } from '@/api/produce/index';
+
+  export default {
+    name: 'outsourceTab',
+    components: {
+      outsourcingDetails,
+      outsourcing,
+      outsourceList,
+      pleaseEntrust,
+      addPlease
+    },
+    props: {
+      // 工单信息
+      workOrderInfo: {
+        type: Object,
+        required: true
+      },
+      // 工序配置 id(作业台取自 store.taskObj.id)
+      produceTaskId: {
+        type: [String, Number],
+        default: ''
+      },
+      // 工序实例 id
+      taskInstanceId: {
+        type: [String, Number],
+        default: ''
+      },
+      // 顶部步骤条当前所选工序的完整数据
+      taskObj: {
+        type: Object,
+        default: () => ({})
+      }
+    },
+    data() {
+      return {
+        checking: false,
+        outsourceAllowed: false,
+        outsourceTipMsg: '',
+        isStep: false,
+        isOutsource: false,
+        isPleaseEntrust: false,
+        chooseType: '1',
+        outsourceForm: {},
+        outObj: {},
+        workInfoList: { list: [] }
+      };
+    },
+    computed: {
+      // 委外面板需要 store taskObj 的字段结构:id 为工序配置 id
+      mergedTaskObj() {
+        return {
+          ...this.taskObj,
+          id: this.produceTaskId,
+          sourceTaskId: this.produceTaskId,
+          taskId: this.taskInstanceId
+        };
+      }
+    },
+    watch: {
+      produceTaskId() {
+        this.reset();
+        this.checkOutsourceEnabled();
+      },
+      workOrderInfo: {
+        deep: true,
+        handler() {
+          this.reset();
+          this.checkOutsourceEnabled();
+        }
+      }
+    },
+    mounted() {
+      this.checkOutsourceEnabled();
+    },
+    methods: {
+      reset() {
+        this.isStep = false;
+        this.isOutsource = false;
+        this.isPleaseEntrust = false;
+        this.chooseType = '1';
+        this.outsourceForm = {};
+        this.outObj = {};
+        this.workInfoList = { list: [] };
+      },
+
+      // 与作业台一致:先判断该工序是否允许委外(pcCheckOutsource)
+      async checkOutsourceEnabled() {
+        if (
+          !this.produceTaskId ||
+          !this.workOrderInfo ||
+          !this.workOrderInfo.id
+        ) {
+          this.outsourceAllowed = false;
+          this.outsourceTipMsg = '暂无工序信息';
+          return;
+        }
+        this.checking = true;
+        try {
+          const res = await pcCheckOutsource({
+            taskId: this.produceTaskId,
+            workOrderId: this.workOrderInfo.id
+          });
+          const data = (res && res.data) || {};
+          this.outsourceForm = { ...data };
+          this.outsourceAllowed = !!data.outsource;
+          this.outsourceTipMsg = this.outsourceAllowed ? '' : '此工序不能委外';
+          this.getTaskInstanceByIdFn(this.workOrderInfo.id);
+        } catch (error) {
+          this.outsourceAllowed = false;
+          this.outsourceTipMsg = '委外资格校验失败';
+        } finally {
+          this.checking = false;
+        }
+      },
+
+      getTaskInstanceByIdFn(workOrderId) {
+        getTaskInstanceById(workOrderId).then((res) => {
+          let { data } = res;
+          if (data && data.length) {
+            data = data.filter(
+              (item) => item.taskId != -2 && item.taskId != -1
+            );
+            const arr = data.findIndex(
+              (item) => item.sourceTaskId == this.produceTaskId
+            );
+            if (arr != -1) {
+              data.splice(0, arr);
+            }
+            this.$set(this.outsourceForm, 'newStepsList', data);
+          }
+        });
+      },
+
+      outsourcingAdd(type, activeName) {
+        this.chooseType = activeName;
+        this.getTaskInstanceByIdFn(this.workOrderInfo.id);
+
+        this.workInfoList = { list: [] };
+        this.workInfoList.list.push(this.workOrderInfo);
+        if (this.chooseType == '2') {
+          this.isStep = true;
+        } else {
+          this.isOutsource = true;
+        }
+      },
+
+      changeCancel(activeType) {
+        this.isStep = false;
+        this.chooseType = activeType;
+        this.reloadList();
+      },
+
+      async changePlugIn(e, index) {
+        if (index == '1') {
+          this.outObj = e;
+          const req = this.buildPlugInReq(e);
+          const res = await checkOutsourceMaterial(req);
+          this.outObj = {
+            ...res.data,
+            ...this.outsourceForm,
+            ...this.outObj
+          };
+          this.isStep = false;
+          this.isOutsource = true;
+        } else if (index == '2') {
+          this.outObj = e;
+          const req = this.buildPlugInReq(e);
+          const res = await checkPleaseEntrust(req);
+          this.outObj = {
+            ...res.data,
+            ...this.outsourceForm,
+            ...this.outObj
+          };
+          this.isStep = false;
+          this.isPleaseEntrust = true;
+        }
+      },
+
+      buildPlugInReq(e) {
+        return {
+          taskId: e.taskId,
+          taskIds: e.taskIds,
+          workOrderId: e.workOrderId,
+          quantity: e.formedNumLast
+        };
+      },
+
+      closeForm() {
+        this.isOutsource = false;
+        this.isPleaseEntrust = false;
+        this.reloadList();
+      },
+
+      outScucc() {
+        this.isOutsource = false;
+        this.isPleaseEntrust = false;
+        this.reloadList();
+      },
+
+      reloadList() {
+        this.$nextTick(() => {
+          const ref = this.$refs.outsourcingListRef;
+          if (!ref || !this.workOrderInfo) {
+            return;
+          }
+          if (this.chooseType == '1') {
+            ref.getDataList(this.workOrderInfo.code);
+          } else if (this.chooseType == '2') {
+            ref.getPleaseData(this.workOrderInfo.code);
+          }
+        });
+      },
+
+      refreshPlease() {
+        this.$nextTick(() => {
+          this.$refs.outsourcingListRef &&
+            this.$refs.outsourcingListRef.getPleaseData(
+              this.workOrderInfo.code
+            );
+        });
+      },
+
+      /** 请托收货成功后刷新工单,通知外层详情抽屉重新拉取 */
+      handlePleaseEntrustReceived() {
+        this.isStep = false;
+        this.isOutsource = false;
+        this.isPleaseEntrust = false;
+        this.$emit('refresh');
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  .operation-tab {
+    min-height: 160px;
+  }
+  .outsource-tip {
+    padding: 12px 0;
+  }
+</style>

+ 70 - 0
src/views/produceOrder/components/details/tabs/taskTab.vue

@@ -0,0 +1,70 @@
+<template>
+  <div class="operation-tab">
+    <!--
+      任务:直接复用 produce 作业台的任务列表面板(inline 模式,不套弹窗)。
+      内含报工 / 接收 / 拒绝 / 报工修改记录等操作。
+    -->
+    <taskDialog v-if="produceTaskId" ref="panel" inline></taskDialog>
+    <el-empty v-else description="暂无工序信息"></el-empty>
+  </div>
+</template>
+
+<script>
+  import taskDialog from '@/views/produce/components/taskDialog/index.vue';
+
+  export default {
+    name: 'taskTab',
+    components: { taskDialog },
+    props: {
+      // 工单信息
+      workOrderInfo: {
+        type: Object,
+        required: true
+      },
+      // 工序配置 id,作业台里取自 store.taskObj.id,这里显式传入
+      produceTaskId: {
+        type: [String, Number],
+        default: ''
+      },
+      // 工序实例 id
+      taskInstanceId: {
+        type: [String, Number],
+        default: ''
+      }
+    },
+    computed: {
+      queryParam() {
+        return {
+          taskId: this.taskInstanceId,
+          produceTaskId: this.produceTaskId,
+          workOrderId: this.workOrderInfo && this.workOrderInfo.id
+        };
+      }
+    },
+    watch: {
+      queryParam() {
+        this.loadPanel();
+      }
+    },
+    mounted() {
+      this.loadPanel();
+    },
+    methods: {
+      loadPanel() {
+        if (!this.produceTaskId || !this.$refs.panel) {
+          return;
+        }
+        // 详情抽屉不会向 store 写入 taskObj,必须显式传 produceTaskId
+        this.$refs.panel.open(this.queryParam, this.workOrderInfo, {
+          produceTaskId: this.produceTaskId
+        });
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  .operation-tab {
+    padding: 0;
+  }
+</style>

+ 38 - 3
src/views/produceOrder/components/recordRules/recordRules.vue

@@ -3,11 +3,12 @@
     <ele-pro-table
       :columns="columns"
       :datasource="datasource"
-      cacheKey="mes-recordRules-25113-031018"
+      :cacheKey="cacheKey"
       ref="tableRef"
     >
       <template v-slot:toolbar>
         <el-select
+          v-if="showTypeSelect"
           v-model="reportWorkType"
           placeholder="请选择"
           @change="reload"
@@ -71,6 +72,14 @@
       workOrderInfo: {
         type: Object,
         required: true
+      },
+      /**
+       * 固定记录类型,传入后隐藏「产前准备/过程控制/产后检查」切换下拉。
+       * 1-产前准备,2-过程控制,3-产后检查;不传则沿用组件内下拉切换(记录详情 tab)
+       */
+      fixedReportWorkType: {
+        type: [Number, String],
+        default: null
       }
     },
     mixins: [tableColumnsMixin, dictMixins],
@@ -138,7 +147,7 @@
             fixed: 'right'
           }
         ],
-        reportWorkType: 1,
+        reportWorkType: this.fixedReportWorkType || 1,
         reportWorkTypeOptions: [
           { label: '产前准备', value: 1 },
           { label: '过程控制', value: 2 },
@@ -147,9 +156,35 @@
         dialogTitle: ''
       };
     },
+    computed: {
+      // 固定类型时不展示切换下拉(由外部 tab 决定展示哪一种)
+      showTypeSelect() {
+        return (
+          this.fixedReportWorkType === null ||
+          this.fixedReportWorkType === undefined ||
+          this.fixedReportWorkType === ''
+        );
+      },
+      // 实际用于查询的记录类型
+      currentReportWorkType() {
+        return this.showTypeSelect
+          ? this.reportWorkType
+          : this.fixedReportWorkType;
+      },
+      // 不同 tab 需要各自的列缓存,避免共用同一 cacheKey
+      cacheKey() {
+        return this.showTypeSelect
+          ? 'mes-recordRules-25113-031018'
+          : `mes-recordRules-fixed-${this.fixedReportWorkType}`;
+      }
+    },
     watch: {
       produceTaskId(newVal) {
         this.reload();
+      },
+      fixedReportWorkType(newVal) {
+        this.reportWorkType = newVal || 1;
+        this.reload();
       }
     },
     methods: {
@@ -161,7 +196,7 @@
           producetaskrulerecordPage({
             workOrderCode: this.workOrderInfo.code,
             produceTaskId: this.produceTaskId,
-            reportWorkType: this.reportWorkType,
+            reportWorkType: this.currentReportWorkType,
             isTempRecord: 0,
             pageNum,
             pageSize