695593266@qq.com hace 3 horas
padre
commit
d9469d600d

+ 12 - 0
src/api/productionPlan/planDotLine.js

@@ -17,3 +17,15 @@ export async function savePlanDotLine(data) {
   }
   return Promise.reject(new Error(res.data.message));
 }
+
+// 根据计划ID和工序ID查询设备快照列表
+export async function listPlanTaskDeviceByPlanIdAndTaskId(data) {
+  const res = await request.post(
+    '/aps/plantaskdevice/listByPlanIdAndTaskId',
+    data
+  );
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}

+ 340 - 55
src/views/produceOrder/components/releaseDialog/planDotLineReleaseDialog.vue

@@ -113,6 +113,9 @@
               cache-key="systemRoleTable"
               row-key="id"
               class="table"
+              :row-class-name="getTableRowClassName"
+              @cell-mouse-enter="handleCellMouseEnter"
+              @cell-mouse-leave="handleCellMouseLeave"
             >
               <template v-slot:toolbar>
                 <div style="display: inline-block" v-if="!item.isSpecialField">
@@ -200,62 +203,128 @@
                   时间段: {{ item.startDate }} ----- {{ item.endDate }}
                 </div>
               </template>
+              <template v-slot:name="{ row }">
+                <el-tooltip
+                  :disabled="!isDeviceTypeLimitedRow(row)"
+                  content="该工序已制定设备类型"
+                  placement="top"
+                  popper-class="device-limited-tooltip-popper"
+                >
+                  <span class="device-limited-tooltip-wrap">
+                    {{ row.name }}
+                    <el-tag
+                      v-if="isDeviceTypeLimitedRow(row)"
+                      class="device-limited-tag"
+                      effect="dark"
+                      size="mini"
+                      type="warning"
+                    >
+                      禁填
+                    </el-tag>
+                  </span>
+                </el-tooltip>
+              </template>
               <template v-slot:quantity="{ row }">
-                <el-input
-                  v-model="row.quantity"
-                  :disabled="row.disposalStatus == 1"
-                  placeholder="请输入数量"
-                  type="number"
-                  @input="(e) => handleQuantityInput(e, row, item)"
-                ></el-input>
+                <el-tooltip
+                  :disabled="!isDeviceTypeLimitedRow(row)"
+                  content="该工序已制定设备类型"
+                  placement="top"
+                  popper-class="device-limited-tooltip-popper"
+                >
+                  <span class="device-limited-tooltip-wrap">
+                    <el-input
+                      v-model="row.quantity"
+                      :disabled="isRowControlDisabled(row)"
+                      placeholder="请输入数量"
+                      type="number"
+                      @input="(e) => handleQuantityInput(e, row, item)"
+                    ></el-input>
+                  </span>
+                </el-tooltip>
               </template>
               <template v-slot:weight="{ row }">
-                <el-input
-                  v-model="row.weight"
-                  :disabled="row.disposalStatus == 1"
-                  placeholder="请输入重量"
-                  type="number"
-                  @input="(e) => handleWeightInput(e, row, item)"
-                ></el-input>
+                <el-tooltip
+                  :disabled="!isDeviceTypeLimitedRow(row)"
+                  content="该工序已制定设备类型"
+                  placement="top"
+                  popper-class="device-limited-tooltip-popper"
+                >
+                  <span class="device-limited-tooltip-wrap">
+                    <el-input
+                      v-model="row.weight"
+                      :disabled="isRowControlDisabled(row)"
+                      placeholder="请输入重量"
+                      type="number"
+                      @input="(e) => handleWeightInput(e, row, item)"
+                    ></el-input>
+                  </span>
+                </el-tooltip>
               </template>
               <template v-slot:teamTimeIds="{ row }">
-                <el-select
-                  v-model="row.teamTimeIds"
-                  multiple
-                  placeholder="班次"
-                  :disabled="row.disposalStatus == 1"
-                  @change="(e) => shiftSelection(e, row, item)"
+                <el-tooltip
+                  :disabled="!isDeviceTypeLimitedRow(row)"
+                  content="该工序已制定设备类型"
+                  placement="top"
+                  popper-class="device-limited-tooltip-popper"
                 >
-                  <el-option
-                    v-for="item in shiftList"
-                    :key="item.id"
-                    :label="item.dutyName"
-                    :value="item.id"
-                  >
-                  </el-option>
-                </el-select>
+                  <span class="device-limited-tooltip-wrap">
+                    <el-select
+                      v-model="row.teamTimeIds"
+                      multiple
+                      placeholder="班次"
+                      :disabled="isRowControlDisabled(row)"
+                      @change="(e) => shiftSelection(e, row, item)"
+                    >
+                      <el-option
+                        v-for="item in shiftList"
+                        :key="item.id"
+                        :label="item.dutyName"
+                        :value="item.id"
+                      >
+                      </el-option>
+                    </el-select>
+                  </span>
+                </el-tooltip>
               </template>
               <template v-slot:startTime="{ row }">
-                <el-date-picker
-                  v-model="row.startTime"
-                  :disabled="row.disposalStatus == 1"
-                  @change="handleStartTimeChange(row, item)"
-                  class="w100"
-                  placeholder="开始时间"
-                  type="datetime"
-                  value-format="yyyy-MM-dd HH:mm:ss"
-                ></el-date-picker>
+                <el-tooltip
+                  :disabled="!isDeviceTypeLimitedRow(row)"
+                  content="该工序已制定设备类型"
+                  placement="top"
+                  popper-class="device-limited-tooltip-popper"
+                >
+                  <span class="device-limited-tooltip-wrap">
+                    <el-date-picker
+                      v-model="row.startTime"
+                      :disabled="isRowControlDisabled(row)"
+                      @change="handleStartTimeChange(row, item)"
+                      class="w100"
+                      placeholder="开始时间"
+                      type="datetime"
+                      value-format="yyyy-MM-dd HH:mm:ss"
+                    ></el-date-picker>
+                  </span>
+                </el-tooltip>
               </template>
               <template v-slot:endTime="{ row }">
-                <el-date-picker
-                  v-model="row.endTime"
-                  :disabled="row.disposalStatus == 1"
-                  @change="handleEndTimeChange(row, item)"
-                  class="w100"
-                  placeholder="完成时间"
-                  type="datetime"
-                  value-format="yyyy-MM-dd HH:mm:ss"
-                ></el-date-picker>
+                <el-tooltip
+                  :disabled="!isDeviceTypeLimitedRow(row)"
+                  content="该工序已制定设备类型"
+                  placement="top"
+                  popper-class="device-limited-tooltip-popper"
+                >
+                  <span class="device-limited-tooltip-wrap">
+                    <el-date-picker
+                      v-model="row.endTime"
+                      :disabled="isRowControlDisabled(row)"
+                      @change="handleEndTimeChange(row, item)"
+                      class="w100"
+                      placeholder="完成时间"
+                      type="datetime"
+                      value-format="yyyy-MM-dd HH:mm:ss"
+                    ></el-date-picker>
+                  </span>
+                </el-tooltip>
               </template>
 
               <template v-slot:action="{ row }">
@@ -315,6 +384,7 @@
     listByFactoryId
   } from '@/api/mainData/index.js';
   import { getUserInfo, listPlanDotLine } from '@/api/produceOrder/index.js';
+  import { listPlanTaskDeviceByPlanIdAndTaskId } from '@/api/productionPlan/planDotLine.js';
   import chooseStation from '../chooseStation.vue';
 
   export default {
@@ -376,7 +446,8 @@
         time_calc_code: '0', // 是否进行时间赋值 0 否 1 是
         validDate,
         userTeamList: [],
-        cachedUserData: null
+        cachedUserData: null,
+        planTaskDeviceMap: {}
       };
     },
     computed: {
@@ -443,11 +514,14 @@
             align: 'center',
             fixed: 'left',
             selectable: (row, index) => {
-              return row.disposalStatus != 1;
+              return (
+                row.disposalStatus != 1 && !this.isDeviceTypeLimitedRow(row)
+              );
             }
           },
           {
             prop: 'name',
+            slot: 'name',
             label: this.dynamicName,
             align: 'center',
             showOverflowTooltip: true,
@@ -556,6 +630,135 @@
         const date = String(now.getDate()).padStart(2, '0');
         return `${year}-${month}-${date}`;
       },
+      normalizeDeviceCode(value) {
+        if (value === null || value === undefined || value === '') {
+          return '';
+        }
+        return String(value).trim();
+      },
+      normalizeDeviceAllowedCodes(list = []) {
+        const codes = new Set();
+        (Array.isArray(list) ? list : []).forEach((item) => {
+          [
+            item?.deviceCode,
+            item?.assetCode,
+            item?.extInfo?.deviceCode,
+            item?.extInfo?.assetCode
+          ].forEach((code) => {
+            const value = this.normalizeDeviceCode(code);
+            if (value) {
+              codes.add(value);
+            }
+          });
+        });
+        return codes;
+      },
+      getPlanTaskDeviceParams(taskId = this.processId) {
+        return {
+          planId: this.current.productionPlanId || this.current.planId || '',
+          taskId
+        };
+      },
+      getPlanTaskDeviceKey(taskId = this.processId) {
+        const params = this.getPlanTaskDeviceParams(taskId);
+        return [params.planId, params.taskId].filter(Boolean).join('::');
+      },
+      getDeviceAllowedCodes(taskId = this.processId) {
+        const key = this.getPlanTaskDeviceKey(taskId);
+        return key ? this.planTaskDeviceMap[key] || new Set() : new Set();
+      },
+      async loadPlanTaskDevices(taskId = this.processId) {
+        const params = this.getPlanTaskDeviceParams(taskId);
+        const key = this.getPlanTaskDeviceKey(taskId);
+        if (!params.planId || !params.taskId || !key) {
+          return new Set();
+        }
+        if (this.planTaskDeviceMap[key]) {
+          return this.planTaskDeviceMap[key];
+        }
+        try {
+          const list = await listPlanTaskDeviceByPlanIdAndTaskId(params);
+          const codes = this.normalizeDeviceAllowedCodes(list);
+          this.$set(this.planTaskDeviceMap, key, codes);
+          return codes;
+        } catch (err) {
+          this.$message.warning(err.message || '获取工序设备类型失败');
+          const codes = new Set();
+          this.$set(this.planTaskDeviceMap, key, codes);
+          return codes;
+        }
+      },
+      isDeviceTypeLimitedRow(row) {
+        return !!row?.__deviceTypeLimited;
+      },
+      getRowDeviceCode(row) {
+        return this.normalizeDeviceCode(
+          row?.assetCode ||
+            row?.deviceCode ||
+            row?.extInfo?.assetCode ||
+            row?.extInfo?.deviceCode ||
+            ''
+        );
+      },
+      isDeviceCodeAllowed(row, codes) {
+        const code = this.getRowDeviceCode(row);
+        return !!code && codes.has(code);
+      },
+      isDeviceTypeLimitedSelection(item, process) {
+        const codes = this.getDeviceAllowedCodes(process?.id || this.processId);
+        if (!codes.size) return false;
+        if (item && typeof item === 'object') {
+          return !this.isDeviceCodeAllowed(item, codes);
+        }
+        const key =
+          item && typeof item === 'object' ? this.getRowUniqueKey(item) : item;
+        const row = (process?.list || []).find(
+          (rowItem) => String(this.getRowUniqueKey(rowItem)) === String(key)
+        );
+        return row ? !this.isDeviceCodeAllowed(row, codes) : false;
+      },
+      isRowControlDisabled(row) {
+        return row.disposalStatus == 1 || this.isDeviceTypeLimitedRow(row);
+      },
+      applyDeviceLimitToList(list = [], dataRow) {
+        if (!dataRow || dataRow.assignType != 1) return list;
+        const codes = this.getDeviceAllowedCodes(dataRow?.id || this.processId);
+        if (!codes.size) return list;
+        return (list || []).map((item) => ({
+          ...item,
+          __deviceTypeLimited: !this.isDeviceCodeAllowed(item, codes)
+        }));
+      },
+      clearDeviceLimitedSelection(process) {
+        if (!process) return;
+        const codes = this.getDeviceAllowedCodes(process.id || this.processId);
+        if (!codes.size) return;
+        const nextSelection = (process.selection || []).filter((item) => {
+          return !this.isDeviceTypeLimitedSelection(item, process);
+        });
+        this.$set(process, 'selection', nextSelection);
+        this.$nextTick(() => {
+          const tab = `tableRef${process.index}`;
+          this.$refs[tab]?.[0]?.setSelectedRowKeys?.(
+            this.getSelectionKeys(nextSelection)
+          );
+        });
+      },
+      getTableRowClassName({ row }) {
+        return this.isDeviceTypeLimitedRow(row)
+          ? 'device-type-limited-row'
+          : '';
+      },
+      handleCellMouseEnter(row, column, cell) {
+        if (this.isDeviceTypeLimitedRow(row) && cell) {
+          cell.setAttribute('title', '该工序已制定设备类型');
+        }
+      },
+      handleCellMouseLeave(row, column, cell) {
+        if (cell?.getAttribute?.('title') === '该工序已制定设备类型') {
+          cell.removeAttribute('title');
+        }
+      },
       async workCenterData() {
         const userData = await getUserInfo(this.$store.state.user.info.userId);
         this.cachedUserData = userData;
@@ -774,7 +977,11 @@
             // status: { code: 0, desc: '未派单' }
           }));
 
-        this.$set(process, 'list', [...oldList, ...newList]);
+        this.$set(
+          process,
+          'list',
+          this.applyDeviceLimitToList([...oldList, ...newList], process)
+        );
 
         const newIds = newList.map((i) => i.id || i.__tempKey);
         if (!process.selection) process.selection = [];
@@ -785,6 +992,7 @@
           this.$refs[tab]?.[0]?.setSelectedRowKeys?.(
             this.getSelectionKeys(process.selection)
           );
+          this.clearDeviceLimitedSelection(process);
         });
       },
 
@@ -985,6 +1193,14 @@
         if (!row.selection || row.selection.length === 0) {
           return this.$message.warning('请最少选择一条数据');
         }
+        if (
+          row.selection.some((item) =>
+            this.isDeviceTypeLimitedSelection(item, row)
+          )
+        ) {
+          this.clearDeviceLimitedSelection(row);
+          return this.$message.warning('该工序已制定设备类型,请勿修改');
+        }
 
         let assignees = [];
         let changeIds = [];
@@ -1099,6 +1315,7 @@
         let id = tab.name;
         this.processId = id;
         await this.FirstTaskIdFn(id);
+        await this.loadPlanTaskDevices(id);
         let data = this.processList.find((item) => item.id == this.processId);
         if (data && (data.type == 2 || data.type == 3 || data.type == 6)) {
           this.$message.warning('请前往质检系统派单');
@@ -1121,20 +1338,21 @@
         }
       },
       // 指派选择
-      changeRadio(e, index) {
+      async changeRadio(e, index) {
         let data = this.processList[index];
         if (e == 1) {
           this.dynamicName = '工位名称';
           data.assignName = '工位';
-          this.getAssignData(index, this.stationList);
+          await this.loadPlanTaskDevices(data.id || this.processId);
+          await this.getAssignData(index, this.stationList);
         } else if (e == 2) {
           this.dynamicName = '人员名称';
           data.assignName = '人员';
-          this.getAssignData(index, this.crewList);
+          await this.getAssignData(index, this.crewList);
         } else {
           this.dynamicName = '产线名称';
           data.assignName = '产线';
-          this.getAssignData(index, this.productionList);
+          await this.getAssignData(index, this.productionList);
         }
       },
       async getAssignData(index, arr, type = 0) {
@@ -1144,6 +1362,7 @@
 
         let list = JSON.parse(JSON.stringify(arr || []));
         list = this.applyExecutionTimeToList(list, dataRow);
+        list = this.applyDeviceLimitToList(list, dataRow);
 
         if (!this.form.teamId) return;
 
@@ -1161,8 +1380,12 @@
 
           if (!res || res.length === 0) {
             // 没有后台数据,合并新增工位
-            const merged = [...list, ...localNewList];
+            const merged = this.applyDeviceLimitToList(
+              [...list, ...localNewList],
+              dataRow
+            );
             this.$set(dataRow, 'list', merged);
+            this.clearDeviceLimitedSelection(dataRow);
             return;
           }
 
@@ -1179,12 +1402,13 @@
           const existingIds = new Set(
             (dataRow.list || []).map((i) => i.id || i.__tempKey)
           );
-          const mergedList = [
+          let mergedList = [
             ...dataRow.list,
             ...syncedLocalNewList.filter(
               (i) => !existingIds.has(i.id || i.__tempKey)
             )
           ];
+          mergedList = this.applyDeviceLimitToList(mergedList, dataRow);
 
           // 更新新增工位状态
           mergedList.forEach((item) => {
@@ -1198,6 +1422,7 @@
           });
 
           this.$set(dataRow, 'list', mergedList);
+          this.clearDeviceLimitedSelection(dataRow);
         } catch (err) {
           this.tabLoading = false;
           this.$message.warning(err.message);
@@ -1315,12 +1540,14 @@
             this.compareEndSetTime(listArr[idx], dataRow);
           }
         });
+        listArr = this.applyDeviceLimitToList(listArr, dataRow);
         //   }
         // });
 
         // console.log(listArr,'listArr')
         this.$set(dataRow, 'list', listArr);
         this.$set(dataRow, 'radioBun', radioBun);
+        this.clearDeviceLimitedSelection(dataRow);
       },
 
       applyExecutionTimeToList(list, dataRow) {
@@ -1460,6 +1687,7 @@
       },
       // 默认选中当前更改数据
       selectedListData(row, item) {
+        if (this.isDeviceTypeLimitedRow(row)) return;
         this.$nextTick(() => {
           const rowKey = this.getRowUniqueKey(row);
           const selectedKeys = this.getSelectionKeys(item.selection || []);
@@ -1740,4 +1968,61 @@
   .describe {
     display: inline-block;
   }
+
+  .device-limited-tooltip-wrap {
+    display: inline-block;
+    width: 100%;
+  }
+
+  .device-limited-tag {
+    margin-left: 8px;
+    border-color: #f59e0b;
+    background-color: #f59e0b;
+    color: #fff;
+    font-weight: 600;
+  }
+
+  ::v-deep .device-type-limited-row > td {
+    position: relative;
+    background-color: #fff7ed !important;
+    color: #9a3412;
+    cursor: not-allowed;
+  }
+
+  ::v-deep .device-type-limited-row:hover > td {
+    background-color: #ffedd5 !important;
+  }
+
+  ::v-deep .device-type-limited-row > td:first-child::before {
+    position: absolute;
+    top: 8px;
+    bottom: 8px;
+    left: 0;
+    width: 4px;
+    background-color: #f59e0b;
+    border-radius: 0 4px 4px 0;
+    content: '';
+  }
+</style>
+
+<style lang="scss">
+  .device-limited-tooltip-popper {
+    max-width: 260px;
+    padding: 9px 12px;
+    border: 1px solid #f59e0b !important;
+    background: #9a3412 !important;
+    color: #fff !important;
+    font-size: 13px;
+    font-weight: 600;
+    line-height: 1.4;
+    box-shadow: 0 8px 22px rgba(154, 52, 18, 0.28);
+  }
+
+  .device-limited-tooltip-popper[x-placement^='top'] .popper__arrow {
+    border-top-color: #f59e0b !important;
+  }
+
+  .device-limited-tooltip-popper[x-placement^='top'] .popper__arrow::after {
+    border-top-color: #9a3412 !important;
+  }
 </style>

+ 6 - 2
src/views/productionPlan/components/gantt/project-gantt.utils.js

@@ -209,11 +209,15 @@ export function buildTooltipHtml(
     },
     {
       label: '开始时间',
-      value: formatDateTime(task.start_date || planMeta.startTime)
+      value: formatDateTime(
+        task.originalStartTime || task.startTime || planMeta.startTime
+      )
     },
     {
       label: '结束时间',
-      value: formatDateTime(task.end_date || planMeta.endTime)
+      value: formatDateTime(
+        task.originalEndTime || task.endTime || planMeta.endTime
+      )
     },
     {
       label: '产品名称',

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

@@ -2076,6 +2076,37 @@
           .join('-');
         return `${parentId}-${periodTaskId || periodIndex}`;
       },
+      parseOrderSchedulingDate(value) {
+        if (!value) {
+          return null;
+        }
+        if (value instanceof Date) {
+          return Number.isFinite(value.getTime()) ? value : null;
+        }
+        const date = new Date(
+          String(value).replace('T', ' ').replace(/-/g, '/')
+        );
+        return Number.isFinite(date.getTime()) ? date : null;
+      },
+      formatOrderSchedulingDayBoundary(date, addDays = 0) {
+        if (!date) {
+          return '';
+        }
+        const value = new Date(date);
+        value.setHours(0, 0, 0, 0);
+        value.setDate(value.getDate() + addDays);
+        const month = String(value.getMonth() + 1).padStart(2, '0');
+        const day = String(value.getDate()).padStart(2, '0');
+        return `${value.getFullYear()}-${month}-${day} 00:00:00`;
+      },
+      getOrderSchedulingFullDayRange(startTime, endTime) {
+        const startDate = this.parseOrderSchedulingDate(startTime);
+        const endDate = this.parseOrderSchedulingDate(endTime) || startDate;
+        return {
+          start_date: this.formatOrderSchedulingDayBoundary(startDate),
+          end_date: this.formatOrderSchedulingDayBoundary(endDate, 1)
+        };
+      },
       async open(row) {
         const ids = Array.isArray(row?.ids) ? row.ids : [];
         const readonly = !!row?.readonly;
@@ -2358,6 +2389,10 @@
             );
             const childDisplayOrderCode =
               this.getOrderSchedulingDisplayOrderCode(matchedPlanInfo, period);
+            const displayRange = this.getOrderSchedulingFullDayRange(
+              period.startTime,
+              period.endTime
+            );
             rows.push({
               id: this.buildGanttPeriodTaskId(
                 parentId,
@@ -2449,8 +2484,10 @@
               resourceCode,
               teamName,
               productionLineName,
-              start_date: formatDotLineDateTime(period.startTime),
-              end_date: formatDotLineDateTime(period.endTime)
+              originalStartTime: formatDotLineDateTime(period.startTime),
+              originalEndTime: formatDotLineDateTime(period.endTime),
+              start_date: displayRange.start_date,
+              end_date: displayRange.end_date
             });
           });
         });

+ 218 - 41
src/views/productionPlan/components/newFactoryProductionScheduling/TaskConfigPanel.vue

@@ -160,8 +160,11 @@
             size="small"
             :row-key="dispatchObjectRowKey"
             :row-class-name="getDispatchTableRowClassName"
+            :cell-class-name="getDispatchTableCellClassName"
             height="100%"
             class="config-table dispatch-table"
+            @cell-mouse-enter="handleDispatchCellMouseEnter"
+            @cell-mouse-leave="handleDispatchCellMouseLeave"
           >
             <el-table-column
               type="index"
@@ -182,12 +185,20 @@
                 />
               </template>
               <template slot-scope="{ row }">
-                <el-checkbox
-                  class="dispatch-state-checkbox"
-                  :value="isSelectedDispatchObject(row)"
-                  :disabled="!isDispatchRowOperable(row)"
-                  @change="handleDispatchObjectCheckChange(row, $event)"
-                />
+                <el-tooltip
+                  :disabled="!isDispatchRowDeviceTypeLimited(row)"
+                  content="该工序已制定设备类型"
+                  placement="top"
+                >
+                  <span class="dispatch-disabled-tooltip-wrap">
+                    <el-checkbox
+                      class="dispatch-state-checkbox"
+                      :value="isSelectedDispatchObject(row)"
+                      :disabled="!isDispatchRowOperable(row)"
+                      @change="handleDispatchObjectCheckChange(row, $event)"
+                    />
+                  </span>
+                </el-tooltip>
               </template>
             </el-table-column>
             <el-table-column
@@ -264,13 +275,21 @@
               width="140"
             >
               <template slot-scope="{ row }">
-                <el-input
-                  :value="row.quantity"
-                  placeholder="请输入数量"
-                  class="config-table-control"
-                  :disabled="isDispatchRowControlDisabled(row)"
-                  @input="handleDispatchRowQuantityInput(row, $event)"
-                />
+                <el-tooltip
+                  :disabled="!isDispatchRowDeviceTypeLimited(row)"
+                  content="该工序已制定设备类型"
+                  placement="top"
+                >
+                  <span class="dispatch-disabled-tooltip-wrap">
+                    <el-input
+                      :value="row.quantity"
+                      placeholder="请输入数量"
+                      class="config-table-control"
+                      :disabled="isDispatchRowControlDisabled(row)"
+                      @input="handleDispatchRowQuantityInput(row, $event)"
+                    />
+                  </span>
+                </el-tooltip>
               </template>
             </el-table-column>
             <el-table-column
@@ -280,16 +299,24 @@
               min-width="220"
             >
               <template slot-scope="{ row }">
-                <el-date-picker
-                  v-model="row.executionStartTime"
-                  type="datetime"
-                  value-format="yyyy-MM-dd HH:mm:ss"
-                  placeholder="开始时间"
-                  clearable
-                  class="config-table-control"
-                  :disabled="isDispatchRowControlDisabled(row)"
-                  @change="handleDispatchRowStartTimeChange(row)"
-                />
+                <el-tooltip
+                  :disabled="!isDispatchRowDeviceTypeLimited(row)"
+                  content="该工序已制定设备类型"
+                  placement="top"
+                >
+                  <span class="dispatch-disabled-tooltip-wrap">
+                    <el-date-picker
+                      v-model="row.executionStartTime"
+                      type="datetime"
+                      value-format="yyyy-MM-dd HH:mm:ss"
+                      placeholder="开始时间"
+                      clearable
+                      class="config-table-control"
+                      :disabled="isDispatchRowControlDisabled(row)"
+                      @change="handleDispatchRowStartTimeChange(row)"
+                    />
+                  </span>
+                </el-tooltip>
               </template>
             </el-table-column>
             <el-table-column
@@ -309,16 +336,24 @@
                     )
                   }"
                 >
-                  <el-date-picker
-                    v-model="row.executionEndTime"
-                    type="datetime"
-                    value-format="yyyy-MM-dd HH:mm:ss"
-                    placeholder="完成时间"
-                    clearable
-                    class="config-table-control"
-                    :disabled="isDispatchRowControlDisabled(row)"
-                    @change="handleDispatchRowEndTimeChange(row)"
-                  />
+                  <el-tooltip
+                    :disabled="!isDispatchRowDeviceTypeLimited(row)"
+                    content="该工序已制定设备类型"
+                    placement="top"
+                  >
+                    <span class="dispatch-disabled-tooltip-wrap">
+                      <el-date-picker
+                        v-model="row.executionEndTime"
+                        type="datetime"
+                        value-format="yyyy-MM-dd HH:mm:ss"
+                        placeholder="完成时间"
+                        clearable
+                        class="config-table-control"
+                        :disabled="isDispatchRowControlDisabled(row)"
+                        @change="handleDispatchRowEndTimeChange(row)"
+                      />
+                    </span>
+                  </el-tooltip>
                 </div>
               </template>
             </el-table-column>
@@ -583,6 +618,7 @@
 
 <script>
   import chooseStation from '@/views/produceOrder/components/chooseStation.vue';
+  import { listPlanTaskDeviceByPlanIdAndTaskId } from '@/api/productionPlan/planDotLine.js';
   import {
     checkAssignConfirm,
     getcheckLoginUserIsTeamLeader,
@@ -718,6 +754,8 @@
         dispatchPageSize: 20,
         dispatchRowDraftMap: {},
         dispatchAssignmentMap: {},
+        dispatchTaskDeviceMap: {},
+        dispatchTaskDeviceLoading: false,
         dispatchToolbarLoading: false,
         dispatchRefreshLoading: false,
         dispatchReportType: 1,
@@ -955,6 +993,7 @@
         this.clearDispatchSelectionsByTaskKey(oldKey);
         this.dispatchPage = 1;
         this.loadDispatchAssignData();
+        this.loadDispatchTaskDevices();
       },
       currentPlan: {
         immediate: true,
@@ -962,8 +1001,10 @@
           this.dispatchReportType = this.getDefaultDispatchReportType();
           this.dispatchRowDraftMap = {};
           this.dispatchAssignmentMap = {};
+          this.dispatchTaskDeviceMap = {};
           this.dispatchPage = 1;
           this.loadDispatchAssignData();
+          this.loadDispatchTaskDevices();
         }
       },
       dispatchObjectRows() {
@@ -995,6 +1036,7 @@
           }
           this.$nextTick(() => {
             this.loadDispatchAssignData();
+            this.loadDispatchTaskDevices();
           });
         }
       }
@@ -1392,6 +1434,82 @@
           '';
         return String(rawId).replace(/^(workstation|person|team):/, '');
       },
+      getDispatchPlanTaskDeviceRequestParams(
+        task = this.activeTask,
+        plan = this.currentPlan || {}
+      ) {
+        return {
+          planId: plan.productionPlanId || plan.planId || plan.id || '',
+          taskId: this.getDispatchSourceTaskId(task)
+        };
+      },
+      getDispatchTaskDeviceKey(task = this.activeTask) {
+        const params = this.getDispatchPlanTaskDeviceRequestParams(task);
+        return [params.planId, params.taskId].filter(Boolean).join('::');
+      },
+      normalizeDispatchDeviceLimitIds(list = []) {
+        const ids = new Set();
+        (Array.isArray(list) ? list : []).forEach((item) => {
+          [
+            item?.id,
+            item?.deviceId,
+            item?.workstationId,
+            item?.factoryWorkstationId,
+            item?.stationId
+          ].forEach((id) => {
+            if (id !== null && id !== undefined && id !== '') {
+              ids.add(String(id));
+            }
+          });
+        });
+        return ids;
+      },
+      getDispatchDeviceLimitIds(task = this.activeTask) {
+        const key = this.getDispatchTaskDeviceKey(task);
+        return key ? this.dispatchTaskDeviceMap[key] || new Set() : new Set();
+      },
+      isDispatchRowDeviceTypeLimited(row) {
+        const ids = this.getDispatchDeviceLimitIds();
+        if (!ids.size) {
+          return false;
+        }
+        return ids.has(String(this.getDispatchAssigneeId(row)));
+      },
+      async loadDispatchTaskDevices() {
+        if (!this.orderDispatchStyle || !this.activeTask || !this.currentPlan) {
+          return;
+        }
+        const params = this.getDispatchPlanTaskDeviceRequestParams();
+        const key = this.getDispatchTaskDeviceKey();
+        if (!params.planId || !params.taskId || !key) {
+          return;
+        }
+        this.dispatchTaskDeviceLoading = true;
+        try {
+          const list = await listPlanTaskDeviceByPlanIdAndTaskId(params);
+          const ids = this.normalizeDispatchDeviceLimitIds(list);
+          this.$set(this.dispatchTaskDeviceMap, key, ids);
+          this.clearDeviceLimitedDispatchRows(ids);
+        } catch (e) {
+          this.$message.warning(e.message || '获取工序设备快照失败');
+        } finally {
+          this.dispatchTaskDeviceLoading = false;
+        }
+      },
+      clearDeviceLimitedDispatchRows(ids = this.getDispatchDeviceLimitIds()) {
+        if (!ids?.size) {
+          return;
+        }
+        this.dispatchObjectRows.forEach((row) => {
+          if (ids.has(String(this.getDispatchAssigneeId(row)))) {
+            this.clearDispatchTaskBinding(row);
+            this.$delete(
+              this.dispatchRowDraftMap,
+              this.dispatchRowDraftKey(row)
+            );
+          }
+        });
+      },
       normalizeDispatchLongId(value) {
         if (value === null || value === undefined || value === '') {
           return '';
@@ -1453,13 +1571,39 @@
         return statusText === '已派单' || statusText === '派单';
       },
       isDispatchRowOperable(row) {
-        return !this.readonlyMode && !this.isDispatchRowAssigned(row);
+        return (
+          !this.readonlyMode &&
+          !this.isDispatchRowAssigned(row) &&
+          !this.isDispatchRowDeviceTypeLimited(row)
+        );
       },
       isDispatchRowControlDisabled(row) {
         return !this.isDispatchRowOperable(row);
       },
       getDispatchTableRowClassName({ row }) {
-        return this.isDispatchRowAssigned(row) ? 'dispatch-row-assigned' : '';
+        return [
+          this.isDispatchRowAssigned(row) ? 'dispatch-row-assigned' : '',
+          this.isDispatchRowDeviceTypeLimited(row)
+            ? 'dispatch-row-device-limited'
+            : ''
+        ]
+          .filter(Boolean)
+          .join(' ');
+      },
+      getDispatchTableCellClassName({ row }) {
+        return this.isDispatchRowDeviceTypeLimited(row)
+          ? 'dispatch-cell-device-limited'
+          : '';
+      },
+      handleDispatchCellMouseEnter(row, column, cell) {
+        if (this.isDispatchRowDeviceTypeLimited(row) && cell) {
+          cell.setAttribute('title', '该工序已制定设备类型');
+        }
+      },
+      handleDispatchCellMouseLeave(row, column, cell) {
+        if (cell?.getAttribute?.('title') === '该工序已制定设备类型') {
+          cell.removeAttribute('title');
+        }
       },
       normalizeDispatchAssignment(item) {
         const assigneeId =
@@ -2669,6 +2813,12 @@
     background: #fff;
   }
 
+  .dispatch-disabled-tooltip-wrap {
+    display: inline-block;
+    width: 100%;
+    cursor: not-allowed;
+  }
+
   .dispatch-table {
     height: 100%;
     border-color: #e5edf6;
@@ -2947,18 +3097,21 @@
       background: #eaf5ff;
     }
 
-    .el-table__body tr.dispatch-row-assigned > td.el-table__cell {
+    .el-table__body tr.dispatch-row-assigned > td.el-table__cell,
+    .el-table__body tr.dispatch-row-device-limited > td.el-table__cell {
       position: relative;
       color: #8c98a6;
       background: #f5f7fa;
       cursor: not-allowed;
     }
 
-    .el-table__body tr.dispatch-row-assigned:hover > td.el-table__cell {
+    .el-table__body tr.dispatch-row-assigned:hover > td.el-table__cell,
+    .el-table__body tr.dispatch-row-device-limited:hover > td.el-table__cell {
       background: #f5f7fa;
     }
 
-    .el-table__body tr.dispatch-row-assigned > td:first-child::before {
+    .el-table__body tr.dispatch-row-assigned > td:first-child::before,
+    .el-table__body tr.dispatch-row-device-limited > td:first-child::before {
       position: absolute;
       top: 8px;
       bottom: 8px;
@@ -2969,6 +3122,10 @@
       content: '';
     }
 
+    .el-table__body tr.dispatch-row-device-limited > td:first-child::before {
+      background: #f59e0b;
+    }
+
     .el-table__body-wrapper::-webkit-scrollbar {
       width: 8px;
       height: 8px;
@@ -3014,23 +3171,43 @@
     }
 
     .dispatch-row-assigned .config-table-control .el-input__inner,
+    .dispatch-row-device-limited .config-table-control .el-input__inner,
     .dispatch-row-assigned
+      .config-table-control.el-date-editor
+      .el-input__inner,
+    .dispatch-row-device-limited
       .config-table-control.el-date-editor
       .el-input__inner {
       color: #9aa6b2;
       border-color: #dcdfe6;
       background: #f5f7fa;
-      box-shadow: inset 3px 0 0 #30b24a;
       cursor: not-allowed;
       -webkit-text-fill-color: #9aa6b2;
     }
 
+    .dispatch-row-assigned .config-table-control .el-input__inner,
+    .dispatch-row-assigned
+      .config-table-control.el-date-editor
+      .el-input__inner {
+      box-shadow: inset 3px 0 0 #30b24a;
+    }
+
+    .dispatch-row-device-limited .config-table-control .el-input__inner,
+    .dispatch-row-device-limited
+      .config-table-control.el-date-editor
+      .el-input__inner {
+      box-shadow: inset 3px 0 0 #f59e0b;
+    }
+
     .dispatch-row-assigned .config-table-control .el-input__prefix,
-    .dispatch-row-assigned .config-table-control .el-input__suffix {
+    .dispatch-row-assigned .config-table-control .el-input__suffix,
+    .dispatch-row-device-limited .config-table-control .el-input__prefix,
+    .dispatch-row-device-limited .config-table-control .el-input__suffix {
       color: #a8b2bf;
     }
 
-    .dispatch-row-assigned .dispatch-state-checkbox {
+    .dispatch-row-assigned .dispatch-state-checkbox,
+    .dispatch-row-device-limited .dispatch-state-checkbox {
       cursor: not-allowed;
     }