695593266@qq.com 10 часов назад
Родитель
Сommit
555ff5eb72

+ 12 - 0
src/api/mainData/index.js

@@ -139,6 +139,18 @@ export async function listAssign(params) {
   return Promise.reject(new Error(res.data.message));
 }
 
+// 根据计划ID和工序ID查询设备快照列表
+export async function listByPlanIdAndTaskId(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));
+}
+
 // 检查派单确认
 export async function checkAssignConfirm(params) {
   const res = await request.get(`/aps/assign/checkAssignConfirm`, { params });

+ 1 - 0
src/views/productionPlan/components/gantt/components/CalendarTable.vue

@@ -302,6 +302,7 @@
     width: 100%;
     min-height: 50px;
     overflow: hidden;
+    cursor: pointer;
   }
 
   .calendar-day-entry {

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

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

+ 100 - 5
src/views/productionPlan/components/gantt/project-gantt.vue

@@ -381,7 +381,11 @@
                       resourceItem.planCode ||
                       resourceItem.productCode ||
                       '',
-                    qty: period.todayRequiredFormingNum ?? '',
+                    qty: this.getCalendarEntryQuantity(
+                      item,
+                      resourceItem,
+                      period
+                    ),
                     productName: resourceItem.productName || '',
                     routingName:
                       resourceItem.routingName ||
@@ -413,7 +417,7 @@
                   resourceList[0]?.routingName ||
                   resourceList[0]?.produceRoutingName ||
                   '',
-                taskLabel: item.taskName || item.name || `工序${index + 1}`,
+                taskLabel: this.getCalendarTaskLabel(item),
                 startDate: periodDates[0],
                 endDate: periodDates[periodDates.length - 1],
                 searchEntries,
@@ -657,6 +661,68 @@
         this.selectedResource =
           this.selectedResource === item.label ? '' : item.label;
       },
+      getCalendarTaskLabel(item = {}) {
+        return String(item.taskName || '').trim();
+      },
+      getCalendarEntryQuantity(item = {}, resourceItem = {}, period = {}) {
+        const planMeta =
+          this.planMetaByCode[resourceItem.planCode] ||
+          this.planMetaByCode[resourceItem.planId] ||
+          this.planMetaByCode[period.planCode] ||
+          this.planMetaByCode[period.planId] ||
+          {};
+        const quantity =
+          period.todayRequiredFormingNum ??
+          period.todayQuantity ??
+          period.dayQuantity ??
+          resourceItem.productNum ??
+          resourceItem.formingNum ??
+          resourceItem.requiredFormingNum ??
+          resourceItem.requiredNum ??
+          resourceItem.planNum ??
+          resourceItem.quantity ??
+          period.productNum ??
+          period.formingNum ??
+          period.requiredFormingNum ??
+          period.requiredNum ??
+          period.planNum ??
+          period.quantity ??
+          '';
+        if (quantity === '' || quantity === null || quantity === undefined) {
+          return '';
+        }
+        const unit =
+          item.measuringUnit ||
+          item.unit ||
+          item.measuringUnitName ||
+          item.unitName ||
+          item.unitMeasurement ||
+          item.productUnit ||
+          resourceItem.measuringUnit ||
+          resourceItem.unit ||
+          resourceItem.measuringUnitName ||
+          resourceItem.unitName ||
+          resourceItem.unitMeasurement ||
+          resourceItem.productUnit ||
+          planMeta.measuringUnit ||
+          planMeta.unit ||
+          planMeta.measuringUnitName ||
+          planMeta.unitName ||
+          planMeta.unitMeasurement ||
+          planMeta.productUnit ||
+          period.measuringUnit ||
+          period.unit ||
+          period.measuringUnitName ||
+          period.unitName ||
+          period.unitMeasurement ||
+          period.productUnit ||
+          '';
+        const text = String(quantity);
+        if (!unit || text.includes(String(unit))) {
+          return text;
+        }
+        return `${text}${unit}`;
+      },
       resetResourceFilter() {
         this.keyword = '';
         this.selectedResource = '';
@@ -1377,9 +1443,38 @@
         // toDo
         // formatGanttData()
 
-        this.ganttData = this.ganttTasks;
+        this.ganttData = this.ganttTasks.map((item) =>
+          this.applyGanttFullDayRange(item)
+        );
         this.reload();
       },
+      formatGanttRenderDate(date) {
+        const pad = (value) => String(value).padStart(2, '0');
+        return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
+          date.getDate()
+        )} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(
+          date.getSeconds()
+        )}`;
+      },
+      applyGanttFullDayRange(task) {
+        const start = parseDate(task.start_date);
+        const end = parseDate(task.end_date);
+        if (!start || !end) {
+          return task;
+        }
+        const renderStart = startOfDay(start);
+        let renderEnd = new Date(startOfDay(end).getTime() + DAY_MS);
+        if (renderEnd.getTime() <= renderStart.getTime()) {
+          renderEnd = new Date(renderStart.getTime() + DAY_MS);
+        }
+        return {
+          ...task,
+          rawStartDate: task.rawStartDate || task.start_date,
+          rawEndDate: task.rawEndDate || task.end_date,
+          start_date: this.formatGanttRenderDate(renderStart),
+          end_date: this.formatGanttRenderDate(renderEnd)
+        };
+      },
       initData() {
         const sourceTasks = this.filteredGanttTasks;
         const parentTaskMap = sourceTasks.reduce((result, item) => {
@@ -1410,7 +1505,7 @@
                 title: mergedCurrent.title || '任务',
                 customClass: 'task-child-plan'
               });
-              return newObj;
+              return this.applyGanttFullDayRange(newObj);
             }
 
             if (mergedCurrent.type == 1) {
@@ -1453,7 +1548,7 @@
                 customClass: 'task-parent-blue'
               });
             }
-            return newObj;
+            return this.applyGanttFullDayRange(newObj);
           });
 
         this.reload();

+ 134 - 6
src/views/productionPlan/components/newFactoryProductionScheduling.vue

@@ -1553,6 +1553,135 @@
       getTaskIndex(row) {
         return this.taskList.findIndex((item) => item === row);
       },
+      getTaskSequenceGroupKey(row) {
+        const key =
+          row?.sourceTaskId ??
+          row?.firstTaskId ??
+          row?.taskId ??
+          row?.id ??
+          row?._taskKey ??
+          '';
+        return key === null || key === undefined ? '' : String(key);
+      },
+      getTaskSequenceSortValue(row, index) {
+        const values = [
+          row?.taskSort,
+          row?.sort,
+          row?.sequence,
+          row?.taskSequence,
+          row?.orderNo,
+          row?.sortNo
+        ];
+        for (const value of values) {
+          if (value !== null && value !== undefined && value !== '') {
+            const num = Number(value);
+            if (!Number.isNaN(num)) {
+              return num;
+            }
+          }
+        }
+        return index;
+      },
+      getTaskSequenceBoundary(group, timeKey, pickLatest) {
+        if (!group?.items?.length) {
+          return null;
+        }
+        return group.items.reduce((best, item, index) => {
+          const value = item?.[timeKey];
+          if (!value) {
+            return best;
+          }
+          const time = new Date(value).getTime();
+          if (Number.isNaN(time)) {
+            return best;
+          }
+          if (!best || (pickLatest ? time > best.time : time < best.time)) {
+            return {
+              item,
+              time,
+              index
+            };
+          }
+          return best;
+        }, null);
+      },
+      getOrderTaskSequenceContext(row) {
+        const currentKey = this.getTaskSequenceGroupKey(row);
+        if (!currentKey) {
+          return null;
+        }
+        const groupMap = new Map();
+        (this.taskList || []).forEach((item, index) => {
+          const key = this.getTaskSequenceGroupKey(item);
+          if (!key) {
+            return;
+          }
+          if (!groupMap.has(key)) {
+            groupMap.set(key, {
+              key,
+              firstIndex: index,
+              sortValue: this.getTaskSequenceSortValue(item, index),
+              items: []
+            });
+          }
+          const group = groupMap.get(key);
+          group.sortValue = Math.min(
+            group.sortValue,
+            this.getTaskSequenceSortValue(item, index)
+          );
+          group.items.push(item);
+        });
+        const groups = Array.from(groupMap.values()).sort(
+          (a, b) => a.sortValue - b.sortValue || a.firstIndex - b.firstIndex
+        );
+        const groupIndex = groups.findIndex((item) => item.key === currentKey);
+        if (groupIndex < 0) {
+          return null;
+        }
+        const prevGroup = groups[groupIndex - 1];
+        const nextGroup = groups[groupIndex + 1];
+        const prevBoundary = this.getTaskSequenceBoundary(
+          prevGroup,
+          'executionEndTime',
+          true
+        );
+        const nextBoundary = this.getTaskSequenceBoundary(
+          nextGroup,
+          'executionStartTime',
+          false
+        );
+        return {
+          index: groupIndex,
+          prev: prevBoundary?.item || null,
+          prevIndex: groupIndex - 1,
+          next: nextBoundary?.item || null,
+          nextIndex: groupIndex + 1
+        };
+      },
+      getTaskSequenceContext(row) {
+        const index = this.getTaskIndex(row);
+        if (index < 0) {
+          return null;
+        }
+        if (!this.isOrderScheduling) {
+          return {
+            index,
+            prev: this.taskList[index - 1],
+            prevIndex: index - 1,
+            next: this.taskList[index + 1],
+            nextIndex: index + 1
+          };
+        }
+        return (
+          this.getOrderTaskSequenceContext(row) || {
+            index,
+            prev: this.taskList[index - 1],
+            prevIndex: index - 1,
+            next: this.taskList[index + 1],
+            nextIndex: index + 1
+          }
+        );
+      },
       compareTaskTime(start, end) {
         if (!start || !end) return false;
         const startTime = new Date(start).getTime();
@@ -1564,12 +1693,11 @@
         );
       },
       validateTaskSequence(row, options = {}) {
-        const index = this.getTaskIndex(row);
-        if (index < 0) {
+        const sequence = this.getTaskSequenceContext(row);
+        if (!sequence) {
           return true;
         }
-        const prev = this.taskList[index - 1];
-        const next = this.taskList[index + 1];
+        const { index, prev, prevIndex, next, nextIndex } = sequence;
         const resetKey = options.resetKey;
         const checkPrev = !resetKey || resetKey === 'executionStartTime';
         const checkNext = !resetKey || resetKey === 'executionEndTime';
@@ -1586,7 +1714,7 @@
               index
             )}执行开始时间不能小于上道工序${this.getTaskLabel(
               prev,
-              index - 1
+              prevIndex
             )}的执行结束时间`
           );
           if (resetKey) {
@@ -1604,7 +1732,7 @@
           this.$message.warning(
             `下道工序${this.getTaskLabel(
               next,
-              index + 1
+              nextIndex
             )}的执行开始时间不能小于上道工序${this.getTaskLabel(
               row,
               index

+ 299 - 47
src/views/productionPlan/components/newFactoryProductionScheduling/TaskConfigPanel.vue

@@ -181,17 +181,28 @@
                   class="dispatch-state-checkbox"
                   :value="isCurrentPageDispatchChecked"
                   :indeterminate="isCurrentPageDispatchIndeterminate"
-                  :disabled="readonlyMode"
+                  :disabled="
+                    readonlyMode || !currentPageSelectableDispatchRows.length
+                  "
                   @change="handleCurrentPageDispatchCheckChange"
                 />
               </template>
               <template slot-scope="{ row }">
-                <el-checkbox
-                  class="dispatch-state-checkbox"
-                  :value="isSelectedDispatchObject(row)"
-                  :disabled="readonlyMode"
-                  @change="handleDispatchObjectCheckChange(row, $event)"
-                />
+                <el-tooltip
+                  :disabled="!isDeviceSnapshotLocked(row)"
+                  content="该工序已制定设备类型"
+                  placement="top"
+                  popper-class="dispatch-device-lock-tooltip"
+                >
+                  <span class="dispatch-checkbox-wrap">
+                    <el-checkbox
+                      class="dispatch-state-checkbox"
+                      :value="isSelectedDispatchObject(row)"
+                      :disabled="isDispatchRowControlDisabled(row)"
+                      @change="handleDispatchObjectCheckChange(row, $event)"
+                    />
+                  </span>
+                </el-tooltip>
               </template>
             </el-table-column>
             <el-table-column
@@ -200,8 +211,28 @@
               label="工位名称"
               align="center"
               min-width="180"
-              show-overflow-tooltip
-            />
+            >
+              <template slot-scope="{ row }">
+                <el-tooltip
+                  :disabled="!isDeviceSnapshotLocked(row) && !row.displayName"
+                  :content="
+                    isDeviceSnapshotLocked(row)
+                      ? '该工序已制定设备类型'
+                      : row.displayName || ''
+                  "
+                  placement="top"
+                  :popper-class="
+                    isDeviceSnapshotLocked(row)
+                      ? 'dispatch-device-lock-tooltip'
+                      : 'dispatch-workstation-tooltip'
+                  "
+                >
+                  <span class="dispatch-workstation-name">
+                    {{ row.displayName }}
+                  </span>
+                </el-tooltip>
+              </template>
+            </el-table-column>
             <el-table-column
               v-if="isDispatchColumnVisible('code')"
               prop="code"
@@ -268,13 +299,22 @@
               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="!isDeviceSnapshotLocked(row)"
+                  content="该工序已制定设备类型"
+                  placement="top"
+                  popper-class="dispatch-device-lock-tooltip"
+                >
+                  <span class="dispatch-disabled-field">
+                    <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
@@ -284,16 +324,25 @@
               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="!isDeviceSnapshotLocked(row)"
+                  content="该工序已制定设备类型"
+                  placement="top"
+                  popper-class="dispatch-device-lock-tooltip"
+                >
+                  <span class="dispatch-disabled-field">
+                    <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
@@ -313,16 +362,25 @@
                     )
                   }"
                 >
-                  <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="!isDeviceSnapshotLocked(row)"
+                    content="该工序已制定设备类型"
+                    placement="top"
+                    popper-class="dispatch-device-lock-tooltip"
+                  >
+                    <span class="dispatch-disabled-field">
+                      <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>
@@ -590,6 +648,7 @@
   import {
     checkAssignConfirm,
     listAssign,
+    listByPlanIdAndTaskId,
     parameterGetByCode,
     resetAssignee,
     taskAssignment,
@@ -724,6 +783,9 @@
         dispatchPageSize: 20,
         dispatchRowDraftMap: {},
         dispatchAssignmentMap: {},
+        dispatchDeviceSnapshotMap: {},
+        dispatchDeviceSnapshotLoading: false,
+        dispatchDeviceSnapshotRequestKey: '',
         dispatchToolbarLoading: false,
         dispatchRefreshLoading: false,
         dispatchReportType: 1,
@@ -919,27 +981,34 @@
         );
       },
       selectedDispatchObjectRows() {
-        return this.dispatchObjectRows.filter((row) =>
-          this.isSelectedDispatchObject(row)
+        return this.dispatchObjectRows.filter(
+          (row) =>
+            !this.isDeviceSnapshotLocked(row) &&
+            this.isSelectedDispatchObject(row)
+        );
+      },
+      currentPageSelectableDispatchRows() {
+        return this.pagedDispatchObjectRows.filter(
+          (row) => !this.isDeviceSnapshotLocked(row)
         );
       },
       currentPageSelectedDispatchRows() {
-        return this.pagedDispatchObjectRows.filter((row) =>
+        return this.currentPageSelectableDispatchRows.filter((row) =>
           this.isSelectedDispatchObject(row)
         );
       },
       isCurrentPageDispatchChecked() {
         return (
-          this.pagedDispatchObjectRows.length > 0 &&
+          this.currentPageSelectableDispatchRows.length > 0 &&
           this.currentPageSelectedDispatchRows.length ===
-            this.pagedDispatchObjectRows.length
+            this.currentPageSelectableDispatchRows.length
         );
       },
       isCurrentPageDispatchIndeterminate() {
         return (
           this.currentPageSelectedDispatchRows.length > 0 &&
           this.currentPageSelectedDispatchRows.length <
-            this.pagedDispatchObjectRows.length
+            this.currentPageSelectableDispatchRows.length
         );
       },
       normalTableHeight() {
@@ -954,6 +1023,7 @@
         this.clearDispatchSelectionsByTaskKey(oldKey);
         this.dispatchPage = 1;
         this.loadDispatchAssignData();
+        this.loadDispatchDeviceSnapshots();
       },
       currentPlan: {
         immediate: true,
@@ -961,8 +1031,10 @@
           this.dispatchReportType = this.getDefaultDispatchReportType();
           this.dispatchRowDraftMap = {};
           this.dispatchAssignmentMap = {};
+          this.dispatchDeviceSnapshotMap = {};
           this.dispatchPage = 1;
           this.loadDispatchAssignData();
+          this.loadDispatchDeviceSnapshots();
         }
       },
       dispatchObjectRows() {
@@ -980,6 +1052,7 @@
           if (!Array.isArray(list) || list.length === 0) {
             this.activeTaskKey = '';
             this.dispatchRowDraftMap = {};
+            this.dispatchDeviceSnapshotMap = {};
             this.dispatchPage = 1;
             return;
           }
@@ -994,6 +1067,7 @@
           }
           this.$nextTick(() => {
             this.loadDispatchAssignData();
+            this.loadDispatchDeviceSnapshots();
           });
         }
       }
@@ -1153,6 +1227,102 @@
         const plan = this.currentPlan || {};
         return plan.workOrderId || plan.orderId || plan.sourceWorkOrderId || '';
       },
+      getDispatchPlanId() {
+        const plan = this.currentPlan || {};
+        return plan.productionPlanId || plan.planId || plan.id || '';
+      },
+      getDeviceSnapshotRequestKey(row = this.activeTask) {
+        const planId = this.getDispatchPlanId();
+        const taskId = this.getDispatchSourceTaskId(row);
+        return planId && taskId ? `${planId}::${taskId}` : '';
+      },
+      normalizeDeviceSnapshotCode(value) {
+        if (value === null || value === undefined || value === '') {
+          return '';
+        }
+        return String(value).trim();
+      },
+      getDispatchDeviceSnapshotCandidateCodes(row) {
+        return [
+          row?.assetCode,
+          row?.deviceCode,
+          row?.extInfo?.assetCode,
+          row?.extInfo?.deviceCode
+        ]
+          .map((item) => this.normalizeDeviceSnapshotCode(item))
+          .filter(Boolean);
+      },
+      isDeviceSnapshotLocked(row) {
+        if (!this.orderDispatchStyle || !row) {
+          return false;
+        }
+        const requestKey = this.getDeviceSnapshotRequestKey();
+        if (
+          !requestKey ||
+          !Object.prototype.hasOwnProperty.call(
+            this.dispatchDeviceSnapshotMap,
+            requestKey
+          )
+        ) {
+          return false;
+        }
+        const snapshotCodes = this.dispatchDeviceSnapshotMap[requestKey] || {};
+        return !this.getDispatchDeviceSnapshotCandidateCodes(row).some(
+          (code) => snapshotCodes[code]
+        );
+      },
+      clearDeviceSnapshotLockedSelections() {
+        this.dispatchObjectRows.forEach((row) => {
+          if (
+            this.isDeviceSnapshotLocked(row) &&
+            this.isSelectedDispatchObject(row)
+          ) {
+            this.clearDispatchObject(row);
+          }
+        });
+      },
+      async loadDispatchDeviceSnapshots() {
+        if (
+          !this.orderDispatchStyle ||
+          this.readonlyMode ||
+          !this.activeTask ||
+          !this.currentPlan
+        ) {
+          return;
+        }
+        const planId = this.getDispatchPlanId();
+        const taskId = this.getDispatchSourceTaskId(this.activeTask);
+        const requestKey = this.getDeviceSnapshotRequestKey(this.activeTask);
+        if (!planId || !taskId || !requestKey) {
+          return;
+        }
+        this.dispatchDeviceSnapshotLoading = true;
+        this.dispatchDeviceSnapshotRequestKey = requestKey;
+        try {
+          const res = await listByPlanIdAndTaskId({ planId, taskId });
+          if (this.dispatchDeviceSnapshotRequestKey !== requestKey) {
+            return;
+          }
+          const snapshotCodes = {};
+          (Array.isArray(res) ? res : []).forEach((item) => {
+            const code = this.normalizeDeviceSnapshotCode(item?.deviceCode);
+            if (code) {
+              snapshotCodes[code] = true;
+            }
+          });
+          this.$set(this.dispatchDeviceSnapshotMap, requestKey, snapshotCodes);
+          this.clearDeviceSnapshotLockedSelections();
+        } catch (e) {
+          if (this.dispatchDeviceSnapshotRequestKey === requestKey) {
+            this.$delete(this.dispatchDeviceSnapshotMap, requestKey);
+          }
+          this.$message.warning(e.message || '获取工序设备快照失败');
+        } finally {
+          if (this.dispatchDeviceSnapshotRequestKey === requestKey) {
+            this.dispatchDeviceSnapshotLoading = false;
+          }
+        }
+      },
       getDispatchWorkCenterId(row = this.activeTask) {
         const plan = this.currentPlan || {};
         return (
@@ -1408,10 +1578,13 @@
         const statusText = this.getDispatchStatusText(data?.status);
         return statusText === '已派单' || statusText === '派单';
       },
-      isDispatchRowControlDisabled() {
-        return this.readonlyMode;
+      isDispatchRowControlDisabled(row) {
+        return this.readonlyMode || this.isDeviceSnapshotLocked(row);
       },
       getDispatchTableRowClassName({ row }) {
+        if (this.isDeviceSnapshotLocked(row)) {
+          return 'dispatch-row-device-locked';
+        }
         return this.readonlyMode && this.isDispatchRowAssigned(row)
           ? 'dispatch-row-assigned'
           : '';
@@ -1688,7 +1861,7 @@
         });
       },
       ensureDispatchObjectSelected(row) {
-        if (!this.activeTask || !row) {
+        if (!this.activeTask || !row || this.isDeviceSnapshotLocked(row)) {
           return null;
         }
         const existed = this.getDispatchRowTask(row);
@@ -1738,7 +1911,7 @@
         row.quantity = this.formatDispatchRowQuantity(task.quantity);
       },
       handleDispatchObjectCheckChange(row, checked) {
-        if (this.readonlyMode) {
+        if (this.isDispatchRowControlDisabled(row)) {
           return;
         }
         if (checked) {
@@ -1907,7 +2080,7 @@
         if (this.readonlyMode) {
           return;
         }
-        this.pagedDispatchObjectRows.forEach((row) => {
+        this.currentPageSelectableDispatchRows.forEach((row) => {
           this.handleDispatchObjectCheckChange(row, checked);
         });
       },
@@ -2906,6 +3079,16 @@
       content: '';
     }
 
+    .el-table__body tr.dispatch-row-device-locked > td.el-table__cell {
+      color: #8a96a3;
+      background: #f4f6f8;
+      cursor: not-allowed;
+    }
+
+    .el-table__body tr.dispatch-row-device-locked:hover > td.el-table__cell {
+      background: #eef2f6;
+    }
+
     .el-table__body-wrapper::-webkit-scrollbar {
       width: 8px;
       height: 8px;
@@ -2972,6 +3155,19 @@
     }
   }
 
+  .dispatch-checkbox-wrap,
+  .dispatch-workstation-name,
+  .dispatch-disabled-field {
+    display: inline-block;
+    width: 100%;
+  }
+
+  .dispatch-workstation-name {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
   @media (max-width: 576px) {
     .config-panel {
       padding: 8px;
@@ -3040,4 +3236,60 @@
       font-size: 13px;
     }
   }
+
+  .dispatch-workstation-tooltip {
+    max-width: 260px;
+    z-index: 4000 !important;
+    line-height: 1.5;
+    word-break: break-all;
+  }
+
+  .dispatch-device-lock-tooltip {
+    min-width: 190px;
+    max-width: 320px;
+    z-index: 7000 !important;
+    padding: 10px 14px 10px 18px;
+    border: 1px solid #ff8a00;
+    border-left: 4px solid #ff5f00;
+    border-radius: 6px;
+    color: #7a2d00 !important;
+    background: linear-gradient(180deg, #fff7dc 0%, #ffe8bd 100%) !important;
+    box-shadow: 0 10px 24px rgba(180, 83, 9, 0.28),
+      inset 0 0 0 1px rgba(255, 255, 255, 0.65);
+    font-size: 14px;
+    font-weight: 700;
+    line-height: 1.45;
+  }
+
+  .dispatch-device-lock-tooltip[x-placement^='top'] .popper__arrow {
+    border-top-color: #ff9f1a !important;
+  }
+
+  .dispatch-device-lock-tooltip[x-placement^='top'] .popper__arrow::after {
+    border-top-color: #fff3dc !important;
+  }
+
+  .dispatch-device-lock-tooltip[x-placement^='bottom'] .popper__arrow {
+    border-bottom-color: #ff9f1a !important;
+  }
+
+  .dispatch-device-lock-tooltip[x-placement^='bottom'] .popper__arrow::after {
+    border-bottom-color: #fff3dc !important;
+  }
+
+  .dispatch-device-lock-tooltip[x-placement^='left'] .popper__arrow {
+    border-left-color: #ff9f1a !important;
+  }
+
+  .dispatch-device-lock-tooltip[x-placement^='left'] .popper__arrow::after {
+    border-left-color: #fff3dc !important;
+  }
+
+  .dispatch-device-lock-tooltip[x-placement^='right'] .popper__arrow {
+    border-right-color: #ff9f1a !important;
+  }
+
+  .dispatch-device-lock-tooltip[x-placement^='right'] .popper__arrow::after {
+    border-right-color: #fff3dc !important;
+  }
 </style>

+ 49 - 4
src/views/workOrder/components/chooseStation.vue

@@ -31,6 +31,22 @@
       <template v-slot:enabled="{ row }">
         {{ dict.enabled[row.enabled] }}
       </template>
+      <template v-slot:name="{ row }">
+        <el-tooltip
+          :disabled="!isDeviceSnapshotLocked(row)"
+          content="该工序已制定设备类型"
+          placement="top"
+          popper-class="device-snapshot-lock-tooltip"
+        >
+          <span
+            :class="{
+              'device-snapshot-locked-text': isDeviceSnapshotLocked(row)
+            }"
+          >
+            {{ row.name }}
+          </span>
+        </el-tooltip>
+      </template>
     </ele-pro-table>
 
     <span slot="footer" class="dialog-footer">
@@ -61,7 +77,8 @@
         index: '',
         sourceTaskId: '',
         type: '',
-        teamId: ''
+        teamId: '',
+        deviceSnapshotLockedCodes: []
       };
     },
 
@@ -98,6 +115,7 @@
           {
             label: '工位名称',
             prop: 'name',
+            slot: 'name',
             showOverflowTooltip: true
           },
           {
@@ -173,12 +191,17 @@
     },
 
     methods: {
-      open(id, list = [], index, type, teamId) {
+      open(id, list = [], index, type, teamId, deviceSnapshotLockedCodes = []) {
         this.dispatchVisible = true;
         this.index = index;
         this.workCenterId = id;
         this.type = type;
         this.teamId = teamId;
+        this.deviceSnapshotLockedCodes = Array.isArray(
+          deviceSnapshotLockedCodes
+        )
+          ? deviceSnapshotLockedCodes.map((item) => String(item).trim())
+          : [];
 
         this.allSelection = Array.isArray(list) ? list : [];
 
@@ -198,7 +221,24 @@
         const existsOld = this.allSelection.some(
           (item) => item.id === row.id && item.isNew !== 1
         );
-        return !existsOld;
+        return !existsOld && !this.isDeviceSnapshotLocked(row);
+      },
+
+      isDeviceSnapshotLocked(row) {
+        if (!row) {
+          return false;
+        }
+        const candidateCodes = [
+          row.assetCode,
+          row.deviceCode,
+          row.extInfo?.assetCode,
+          row.extInfo?.deviceCode
+        ]
+          .filter((item) => item !== null && item !== undefined && item !== '')
+          .map((item) => String(item).trim());
+        return !candidateCodes.some((code) =>
+          this.deviceSnapshotLockedCodes.includes(code)
+        );
       },
 
       cancel() {
@@ -244,4 +284,9 @@
   };
 </script>
 
-<style></style>
+<style scoped>
+  .device-snapshot-locked-text {
+    color: #8a96a3;
+    cursor: not-allowed;
+  }
+</style>

+ 65 - 6
src/views/workOrder/components/planDotLinReleaseDialog.vue

@@ -294,9 +294,30 @@
                   时间段: {{ item.startDate }} ----- {{ item.endDate }}
                 </div>
               </template>
+              <template v-slot:name="{ row }">
+                <el-tooltip
+                  :disabled="!isDeviceSnapshotLocked(row, item)"
+                  :content="getDeviceSnapshotLockTip()"
+                  placement="top"
+                  popper-class="device-snapshot-lock-tooltip"
+                >
+                  <span
+                    :class="{
+                      'device-snapshot-locked-text': isDeviceSnapshotLocked(
+                        row,
+                        item
+                      )
+                    }"
+                  >
+                    {{ row.name }}
+                  </span>
+                </el-tooltip>
+              </template>
               <template v-slot:quantity="{ row }">
                 <el-input
-                  :disabled="permissions(row)"
+                  :disabled="
+                    permissions(row) || isDeviceSnapshotLocked(row, item)
+                  "
                   type="number"
                   v-model="row.quantity"
                   placeholder="请输入数量"
@@ -305,7 +326,9 @@
               </template>
               <template v-slot:weight="{ row }">
                 <el-input
-                  :disabled="permissions(row)"
+                  :disabled="
+                    permissions(row) || isDeviceSnapshotLocked(row, item)
+                  "
                   type="number"
                   v-model="row.weight"
                   placeholder="请输入重量"
@@ -314,7 +337,9 @@
               </template>
               <template v-slot:teamTimeIds="{ row }">
                 <el-select
-                  :disabled="permissions(row)"
+                  :disabled="
+                    permissions(row) || isDeviceSnapshotLocked(row, item)
+                  "
                   multiple
                   v-model="row.teamTimeIds"
                   placeholder="班次"
@@ -331,7 +356,9 @@
               </template>
               <template v-slot:startTime="{ row }">
                 <el-date-picker
-                  :disabled="permissions(row)"
+                  :disabled="
+                    permissions(row) || isDeviceSnapshotLocked(row, item)
+                  "
                   class="w100"
                   v-model="row.startTime"
                   type="datetime"
@@ -342,7 +369,9 @@
               </template>
               <template v-slot:endTime="{ row }">
                 <el-date-picker
-                  :disabled="permissions(row)"
+                  :disabled="
+                    permissions(row) || isDeviceSnapshotLocked(row, item)
+                  "
                   class="w100"
                   v-model="row.endTime"
                   type="datetime"
@@ -570,9 +599,11 @@
               columnKey: 'selection',
               align: 'center',
               fixed: 'left',
-              reserveSelection: true
+              reserveSelection: true,
+              selectable: (row) => !this.isDeviceSnapshotLocked(row, val)
             },
             {
+              slot: 'name',
               prop: 'name',
               label: this.dynamicName,
               align: 'center',
@@ -730,6 +761,11 @@
     margin-top: 20px;
   }
 
+  .device-snapshot-locked-text {
+    color: #8a96a3;
+    cursor: not-allowed;
+  }
+
   ::v-deep .el-radio-button__orig-radio:checked + .el-radio-button__inner {
     background-color: #10d070;
     border-color: #10d070;
@@ -771,3 +807,26 @@
   //   }
   // }
 </style>
+
+<style lang="scss">
+  .device-snapshot-lock-tooltip.el-tooltip__popper {
+    padding: 10px 14px;
+    color: #7a2e00;
+    font-size: 14px;
+    font-weight: 600;
+    line-height: 1.4;
+    background: #fff4e5;
+    border: 1px solid #ff9f43;
+    box-shadow: 0 6px 18px rgba(122, 46, 0, 0.22);
+  }
+
+  .device-snapshot-lock-tooltip.el-tooltip__popper[x-placement^='top']
+    .popper__arrow {
+    border-top-color: #ff9f43;
+  }
+
+  .device-snapshot-lock-tooltip.el-tooltip__popper[x-placement^='top']
+    .popper__arrow::after {
+    border-top-color: #fff4e5;
+  }
+</style>

+ 120 - 3
src/views/workOrder/mixins/planDotLineRelease.js

@@ -14,7 +14,8 @@ import {
   listByFactoryId,
   checkExists,
   parameterGetByCode,
-  listPlanDotLine
+  listPlanDotLine,
+  listByPlanIdAndTaskId
 } from '@/api/mainData/index.js';
 import {
   releaseWorkOrder,
@@ -25,7 +26,10 @@ import {
 export default {
   data() {
     return {
-      isTpye: ''
+      isTpye: '',
+      deviceSnapshotMap: {},
+      deviceSnapshotRequestKey: '',
+      deviceSnapshotLoading: false
     };
   },
 
@@ -218,6 +222,7 @@ export default {
           if (isExist) {
             this.processList = list;
             this.processId = res[0].sourceTaskId;
+            await this.loadDeviceSnapshots(this.processList[0]);
             // this.handleClick({ name: res[0].taskId });
             this.initializeQuery();
           } else {
@@ -387,6 +392,116 @@ export default {
       const statusCode = Number(row?.status?.code);
       return row?.isNew === 1 && statusCode !== 0 && statusCode !== 1;
     },
+    getCurrentPlanId() {
+      return (
+        this.current.productionPlanId ||
+        this.current.planId ||
+        this.current.producePlanId ||
+        ''
+      );
+    },
+    getDeviceSnapshotRequestKey(taskId) {
+      const planId = this.getCurrentPlanId();
+      return planId && taskId ? `${planId}::${taskId}` : '';
+    },
+    normalizeDeviceSnapshotCode(value) {
+      if (value === null || value === undefined || value === '') {
+        return '';
+      }
+      return String(value).trim();
+    },
+    getDeviceSnapshotCandidateCodes(row) {
+      return [
+        row?.assetCode,
+        row?.deviceCode,
+        row?.extInfo?.assetCode,
+        row?.extInfo?.deviceCode
+      ]
+        .map((item) => this.normalizeDeviceSnapshotCode(item))
+        .filter(Boolean);
+    },
+    isDeviceSnapshotLocked(row, process = null) {
+      if (!row) {
+        return false;
+      }
+      const taskId = process?.id || this.processId;
+      const requestKey = this.getDeviceSnapshotRequestKey(taskId);
+      if (
+        !requestKey ||
+        !Object.prototype.hasOwnProperty.call(
+          this.deviceSnapshotMap,
+          requestKey
+        )
+      ) {
+        return false;
+      }
+      const snapshotCodes = this.deviceSnapshotMap[requestKey] || {};
+      return !this.getDeviceSnapshotCandidateCodes(row).some(
+        (code) => snapshotCodes[code]
+      );
+    },
+    getDeviceSnapshotLockTip() {
+      return '该工序已制定设备类型';
+    },
+    getDeviceSnapshotLockedCodes(process) {
+      const requestKey = this.getDeviceSnapshotRequestKey(
+        process?.id || this.processId
+      );
+      return Object.keys(this.deviceSnapshotMap[requestKey] || {});
+    },
+    clearDeviceSnapshotLockedSelections(process) {
+      if (!process) {
+        return;
+      }
+      const selection = Array.isArray(process.selection)
+        ? process.selection.filter(
+            (row) => !this.isDeviceSnapshotLocked(row, process)
+          )
+        : [];
+      this.$set(process, 'selection', selection);
+      this.$nextTick(() => {
+        const tab = `tableRef${process.index}`;
+        const tableRefs = this.$refs[tab];
+        if (!tableRefs || !tableRefs[0]) {
+          return;
+        }
+        tableRefs[0].setSelectedRowKeys(selection.map((row) => row.id));
+      });
+    },
+    async loadDeviceSnapshots(process) {
+      const taskId = process?.id || this.processId;
+      const planId = this.getCurrentPlanId();
+      const requestKey = this.getDeviceSnapshotRequestKey(taskId);
+      if (!planId || !taskId || !requestKey) {
+        return;
+      }
+      this.deviceSnapshotLoading = true;
+      this.deviceSnapshotRequestKey = requestKey;
+      try {
+        const res = await listByPlanIdAndTaskId({ planId, taskId });
+        if (this.deviceSnapshotRequestKey !== requestKey) {
+          return;
+        }
+        const snapshotCodes = {};
+        (Array.isArray(res) ? res : []).forEach((item) => {
+          const code = this.normalizeDeviceSnapshotCode(item?.deviceCode);
+          if (code) {
+            snapshotCodes[code] = true;
+          }
+        });
+        this.$set(this.deviceSnapshotMap, requestKey, snapshotCodes);
+        this.clearDeviceSnapshotLockedSelections(process);
+      } catch (err) {
+        if (this.deviceSnapshotRequestKey === requestKey) {
+          this.$set(this.deviceSnapshotMap, requestKey, {});
+        }
+        this.$message.warning(err.message || '获取工序设备类型数据失败');
+      } finally {
+        if (this.deviceSnapshotRequestKey === requestKey) {
+          this.deviceSnapshotLoading = false;
+        }
+      }
+    },
     refreshProcessDateRange(item) {
       this.$set(item, 'startDate', '');
       this.$set(item, 'endDate', '');
@@ -590,7 +705,8 @@ export default {
         item.list,
         index,
         1,
-        this.form.teamId
+        this.form.teamId,
+        this.getDeviceSnapshotLockedCodes(item)
       );
     },
 
@@ -842,6 +958,7 @@ export default {
       if (!data) {
         return;
       }
+      await this.loadDeviceSnapshots(data);
       this.form.teamName = data.executionTeamName || '';
       this.form.teamId = data.executionTeamId || '';
       await this.changeRadio(data.assignType, data.index);