浏览代码

修改工厂排产显示的问题

695593266@qq.com 3 月之前
父节点
当前提交
2fd3cb0a84

+ 180 - 34
src/views/productionPlan/components/gantt/project-gantt.vue

@@ -51,12 +51,45 @@
           </div>
         </div>
       </div>
-      <div
-        v-show="viewMode === 'gantt'"
-        ref="gantt_scrollbar"
-        class="gantt-box"
-      >
-        <div ref="gantt" class="gantt-container"></div>
+      <div v-show="viewMode === 'gantt'" class="gantt-view">
+        <div class="calendar-toolbar">
+          <div class="calendar-filters">
+            <div class="calendar-filter-item">
+              <span class="calendar-filter-label">工艺路线名称:</span>
+              <el-input
+                v-model.trim="ganttQuery.routingName"
+                size="small"
+                clearable
+                placeholder="请输入"
+              ></el-input>
+            </div>
+            <div class="calendar-filter-item">
+              <span class="calendar-filter-label">产品名称:</span>
+              <el-input
+                v-model.trim="ganttQuery.productName"
+                size="small"
+                clearable
+                placeholder="请输入"
+              ></el-input>
+            </div>
+            <div class="calendar-filter-item">
+              <span class="calendar-filter-label">计划编号:</span>
+              <el-input
+                v-model.trim="ganttQuery.planCode"
+                size="small"
+                clearable
+                placeholder="请输入"
+              ></el-input>
+            </div>
+            <el-button size="small" type="primary" @click="applyGanttFilters">
+              搜索
+            </el-button>
+            <el-button size="small" @click="resetGanttFilters">重置</el-button>
+          </div>
+        </div>
+        <div ref="gantt_scrollbar" class="gantt-box">
+          <div ref="gantt" class="gantt-container"></div>
+        </div>
       </div>
       <div v-show="viewMode === 'calendar'" class="calendar-view">
         <div class="calendar-toolbar">
@@ -65,7 +98,7 @@
               <span class="calendar-filter-label">工艺路线名称:</span>
               <el-input
                 v-model.trim="calendarQuery.routingName"
-                size="mini"
+                size="small"
                 clearable
                 placeholder="请输入"
               ></el-input>
@@ -74,7 +107,7 @@
               <span class="calendar-filter-label">产品名称:</span>
               <el-input
                 v-model.trim="calendarQuery.productName"
-                size="mini"
+                size="small"
                 clearable
                 placeholder="请输入"
               ></el-input>
@@ -83,15 +116,15 @@
               <span class="calendar-filter-label">计划编号:</span>
               <el-input
                 v-model.trim="calendarQuery.planCode"
-                size="mini"
+                size="small"
                 clearable
                 placeholder="请输入"
               ></el-input>
             </div>
-            <el-button size="mini" type="primary" @click="applyCalendarFilters">
+            <el-button size="small" type="primary" @click="applyCalendarFilters">
               搜索
             </el-button>
-            <el-button size="mini" @click="resetCalendarFilters">重置</el-button>
+            <el-button size="small" @click="resetCalendarFilters">重置</el-button>
           </div>
         </div>
         <div class="calendar-card">
@@ -248,6 +281,16 @@
         selectedResource: '',
         keyword: '',
         viewMode: 'gantt',
+        ganttQuery: {
+          routingName: '',
+          productName: '',
+          planCode: ''
+        },
+        ganttFilters: {
+          routingName: '',
+          productName: '',
+          planCode: ''
+        },
         calendarQuery: {
           routingName: '',
           productName: '',
@@ -310,7 +353,10 @@
         );
       },
       filteredGanttTasks() {
-        return this.ganttTasks.filter((item) => this.matchesResourceFilter(item));
+        return this.ganttTasks.filter(
+          (item) =>
+            this.matchesResourceFilter(item) && this.matchesGanttFilters(item)
+        );
       },
       planMetaByCode() {
         return this.planList.reduce((result, item) => {
@@ -744,6 +790,27 @@
           ...this.calendarQuery
         };
       },
+      applyGanttFilters() {
+        this.ganttFilters = {
+          ...this.ganttQuery
+        };
+        this.hideHoverTooltip();
+        this.initData();
+      },
+      resetGanttFilters() {
+        this.ganttQuery = {
+          routingName: '',
+          productName: '',
+          planCode: ''
+        };
+        this.ganttFilters = {
+          routingName: '',
+          productName: '',
+          planCode: ''
+        };
+        this.hideHoverTooltip();
+        this.initData();
+      },
       resetCalendarFilters() {
         this.calendarQuery = {
           routingName: '',
@@ -757,33 +824,56 @@
         };
         this.calendarExpand = true;
       },
-      matchesCalendarFilters(entry) {
+      normalizePlanSearchEntry(entry) {
+        const planKey = entry.planCode || entry.code || entry.id;
+        const planMeta = this.planMetaByCode[planKey] || {};
+        return {
+          routingName:
+            entry.routingName ||
+            entry.produceRoutingName ||
+            planMeta.routingName ||
+            planMeta.produceRoutingName ||
+            planMeta.routeName ||
+            '',
+          productName: entry.productName || planMeta.productName || '',
+          planCode:
+            entry.planCode || entry.code || planMeta.code || planMeta.planCode || ''
+        };
+      },
+      matchesPlanSearchFilters(entry, filters) {
+        const normalizedEntry = this.normalizePlanSearchEntry(entry || {});
         if (
-          this.calendarFilters.routingName &&
-          !String(entry.routingName || '')
+          filters.routingName &&
+          !String(normalizedEntry.routingName || '')
             .toLowerCase()
-            .includes(this.calendarFilters.routingName.toLowerCase())
+            .includes(filters.routingName.toLowerCase())
         ) {
           return false;
         }
         if (
-          this.calendarFilters.productName &&
-          !String(entry.productName || '')
+          filters.productName &&
+          !String(normalizedEntry.productName || '')
             .toLowerCase()
-            .includes(this.calendarFilters.productName.toLowerCase())
+            .includes(filters.productName.toLowerCase())
         ) {
           return false;
         }
         if (
-          this.calendarFilters.planCode &&
-          !String(entry.planCode || '')
+          filters.planCode &&
+          !String(normalizedEntry.planCode || '')
             .toLowerCase()
-            .includes(this.calendarFilters.planCode.toLowerCase())
+            .includes(filters.planCode.toLowerCase())
         ) {
           return false;
         }
         return true;
       },
+      matchesGanttFilters(item) {
+        return this.matchesPlanSearchFilters(item, this.ganttFilters);
+      },
+      matchesCalendarFilters(entry) {
+        return this.matchesPlanSearchFilters(entry, this.calendarFilters);
+      },
       buildCalendarTooltipHtml(entries) {
         const list = Array.isArray(entries) ? entries : [entries];
         const blocks = list
@@ -1311,6 +1401,61 @@
     display: flex;
     flex-direction: column;
     gap: 12px;
+    .calendar-filter-label {
+      font-size: 14px;
+    }
+    .calendar-empty {
+      font-size: 16px;
+    }
+    .calendar-th {
+      height: 36px;
+      font-size: 14px;
+    }
+    .calendar-th--month {
+      height: 40px;
+      font-size: 22px;
+    }
+    .calendar-th--group,
+    .calendar-td--group {
+      width: 108px;
+      min-width: 108px;
+    }
+    .calendar-th--task,
+    .calendar-td--task {
+      width: 184px;
+      min-width: 184px;
+    }
+    .calendar-th--day {
+      width: 48px;
+      min-width: 48px;
+      font-size: 14px;
+    }
+    .calendar-td--group,
+    .calendar-td--task {
+      font-size: 15px;
+    }
+    .calendar-td--day {
+      height: 56px;
+      padding: 3px 5px;
+    }
+    .calendar-day-content {
+      min-height: 48px;
+    }
+    .calendar-day-entry {
+      font-size: 14px;
+      line-height: 17px;
+    }
+  }
+  .gantt-view {
+    flex: 1;
+    min-width: 0;
+    min-height: 0;
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+    .calendar-filter-label {
+      font-size: 14px;
+    }
   }
   .calendar-toolbar {
     padding: 12px 16px;
@@ -1378,15 +1523,15 @@
     padding: 0;
   }
   .calendar-th {
-    height: 30px;
+    height: 34px;
     background: #f8f8f8;
     color: #303133;
     font-weight: 500;
-    font-size: 11px;
+    font-size: 13px;
   }
   .calendar-th--month {
-    height: 34px;
-    font-size: 18px;
+    height: 38px;
+    font-size: 20px;
     font-weight: 500;
     background: #efefef;
   }
@@ -1401,8 +1546,9 @@
     min-width: 170px;
   }
   .calendar-th--day {
-    width: 36px;
-    min-width: 36px;
+    width: 44px;
+    min-width: 44px;
+    font-size: 13px;
   }
   .calendar-th--day.is-weekend {
     background: #f4f4f4;
@@ -1416,7 +1562,7 @@
     padding: 6px 8px;
     background: #fff;
     color: #303133;
-    font-size: 12px;
+    font-size: 14px;
     word-break: break-all;
     text-align: left;
     vertical-align: middle;
@@ -1425,10 +1571,10 @@
     font-weight: 500;
   }
   .calendar-td--day {
-    height: 44px;
+    height: 52px;
     background: #fff;
     vertical-align: top;
-    padding: 2px 3px;
+    padding: 3px 4px;
   }
   .calendar-td--day.is-weekend {
     background: #fafafa;
@@ -1438,7 +1584,7 @@
   }
   .calendar-day-content {
     width: 100%;
-    min-height: 38px;
+    min-height: 44px;
     overflow: hidden;
   }
   .calendar-day-entry {
@@ -1446,8 +1592,8 @@
     align-items: center;
     justify-content: space-between;
     gap: 4px;
-    font-size: 10px;
-    line-height: 12px;
+    font-size: 13px;
+    line-height: 16px;
     color: #666;
     white-space: nowrap;
     overflow: hidden;

+ 469 - 23
src/views/productionPlan/components/newFactoryProductionScheduling.vue

@@ -14,8 +14,26 @@
       :planList="planDataList"
       :calendarSourceData="calendarSourceData"
     ></projectGantt>
-    <div style="display: flex">
-      <div class="form-wrapper" style="width: calc(100% - 600px)">
+    <div class="scheduling-content">
+      <div class="form-wrapper">
+        <div class="btnList">
+          <el-button
+            type="primary"
+            size="mini"
+            :disabled="entryReadonly"
+            @click="openPlanPicker(1, '添加生产计划')"
+          >
+            添加生产计划
+          </el-button>
+          <el-button
+            type="primary"
+            size="mini"
+            :disabled="entryReadonly"
+            @click="openPlanPicker(3, '添加临时计划')"
+          >
+            添加临时计划
+          </el-button>
+        </div>
         <!-- <div class="btnList">
           <el-button type="primary" @click="confirm">保存</el-button>
           <el-popconfirm
@@ -53,7 +71,7 @@
           ref="planTable"
           :columns="columns"
           :datasource="planDataList"
-          row-key="code"
+          row-key="id"
           :selection.sync="selection"
           @update:selection="handlePlanSelectionChange"
           height="400px"
@@ -62,11 +80,10 @@
         ></ele-pro-table>
       </div>
       <div
-        style="width: 600px"
         v-loading="dotLineLoading"
         :class="['plan-dot-line-side', { 'is-fullscreen': dotLineFullscreen }]"
       >
-        <div class="pane-box plan-dot-line-detail">
+        <div class="plan-dot-line-detail">
           <div class="plan-time-bar">
             <span class="plan-time-item">
               <span class="plan-time-label">计划开始时间:</span>
@@ -81,7 +98,7 @@
               <span class="plan-time-value">{{ planNumDisplay }}</span>
             </span>
           </div>
-          <div class="plan-dot-line">
+          <div class="plan-dot-line-scrollable">
             <div class="top-route">
               <div class="panel-title">工艺路线</div>
               <el-empty
@@ -108,6 +125,9 @@
               <div class="panel-header">
                 <div class="panel-title">工艺配置</div>
                 <div class="panel-actions">
+                  <span v-if="isCurrentPlanReadonly" class="readonly-tip">
+                    当前状态仅支持查看
+                  </span>
                   <el-tooltip
                     :content="dotLineFullscreen ? '退出全屏' : '全屏展示'"
                     placement="top"
@@ -129,7 +149,9 @@
                   <el-button
                     type="primary"
                     size="mini"
-                    :disabled="!currentPlan || dotLineLoading"
+                    :disabled="
+                      !currentPlan || dotLineLoading || isCurrentPlanReadonly
+                    "
                     @click="submitDotLineConfig"
                   >
                     确定
@@ -179,6 +201,7 @@
                         v-model="row.executionType"
                         placeholder="执行模式"
                         clearable
+                        :disabled="isCurrentPlanReadonly"
                         class="config-table-control"
                         @change="onExecutionTypeChange(row)"
                       >
@@ -203,6 +226,7 @@
                         placeholder="请选择"
                         clearable
                         filterable
+                        :disabled="isCurrentPlanReadonly"
                         class="config-table-control"
                         @change="onExecutionObjectChange(row)"
                       >
@@ -219,6 +243,7 @@
                         placeholder="请选择"
                         clearable
                         filterable
+                        :disabled="isCurrentPlanReadonly"
                         class="config-table-control"
                         @change="onExecutionObjectChange(row)"
                       >
@@ -257,6 +282,7 @@
                         value-format="yyyy-MM-dd HH:mm:ss"
                         placeholder="执行开始时间"
                         clearable
+                        :disabled="isCurrentPlanReadonly"
                         class="config-table-control"
                         @change="handleTimeChange(row, 'executionStartTime')"
                       />
@@ -282,6 +308,7 @@
                           value-format="yyyy-MM-dd HH:mm:ss"
                           placeholder="执行结束时间"
                           clearable
+                          :disabled="isCurrentPlanReadonly"
                           class="config-table-control"
                           @change="handleTimeChange(row, 'executionEndTime')"
                         />
@@ -297,16 +324,98 @@
     </div>
 
     <div slot="footer">
-      <el-button plain @click="cancel">取消</el-button>
+      <!-- <el-button plain @click="cancel">取消</el-button> -->
       <el-button @click="cancel">关闭</el-button>
     </div>
+
+    <ele-modal
+      :title="planPickerTitle"
+      :visible.sync="planPickerVisible"
+      append-to-body
+      width="88%"
+      :close-on-click-modal="false"
+      :maxable="true"
+    >
+      <div class="plan-picker-search">
+        <div class="plan-picker-search__item">
+          <span class="plan-picker-search__label">计划编号:</span>
+          <el-input
+            v-model.trim="planPickerWhere.code"
+            size="mini"
+            clearable
+            placeholder="请输入"
+          />
+        </div>
+        <div class="plan-picker-search__item">
+          <span class="plan-picker-search__label">产品编码:</span>
+          <el-input
+            v-model.trim="planPickerWhere.productCode"
+            size="mini"
+            clearable
+            placeholder="请输入"
+          />
+        </div>
+        <div class="plan-picker-search__item">
+          <span class="plan-picker-search__label">状态:</span>
+          <el-select
+            v-model="planPickerWhere.status"
+            size="mini"
+            clearable
+            placeholder="请选择"
+          >
+            <el-option
+              v-for="item in planPickerStatusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+        <div class="plan-picker-search__item">
+          <span class="plan-picker-search__label">批次号:</span>
+          <el-input
+            v-model.trim="planPickerWhere.batchNo"
+            size="mini"
+            clearable
+            placeholder="请输入"
+          />
+        </div>
+        <div class="plan-picker-search__actions">
+          <el-button size="mini" type="primary" @click="handlePlanPickerSearch">
+            搜索
+          </el-button>
+          <el-button size="mini" @click="resetPlanPickerSearch">重置</el-button>
+        </div>
+      </div>
+      <div v-loading="planPickerLoading">
+        <ele-pro-table
+          ref="planPickerTable"
+          width="100%"
+          :columns="columns"
+          :datasource="planPickerDatasource"
+          row-key="id"
+          :selection.sync="planPickerSelection"
+          @update:selection="handlePlanPickerSelectionChange"
+          height="520px"
+          full-height="calc(100vh - 220px)"
+          :page-size="20"
+        ></ele-pro-table>
+      </div>
+      <div slot="footer">
+        <el-button @click="planPickerVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmPlanPicker">确定</el-button>
+      </div>
+    </ele-modal>
   </ele-modal>
 </template>
 
 <script>
   import projectGantt from './gantt/project-gantt.vue';
   // import dayjs from 'dayjs';
-  import { teamSchedulingGanttChart } from '@/api/productionPlan/index.js';
+  import {
+    getList,
+    teamSchedulingGanttChart
+  } from '@/api/productionPlan/index.js';
   import { getTaskListById } from '@/api/materialPlan';
   import { teamPage } from '@/api/mainData';
   import { getFactoryarea } from '@/api/saleOrder';
@@ -318,6 +427,13 @@
 
   const EXEC_TYPE_MAP = { 0: '自制', 1: '请托', 2: '委外' };
   const EXEC_TYPE = Object.freeze({ HOMEMADE: 0, ENTRUST: 1, OUTSOURCE: 2 });
+  const READONLY_LAYOUT_STATUSES = Object.freeze(['7', '4', '5', '6']);
+  const PLAN_PICKER_STATUS_OPTIONS = Object.freeze([
+    { label: '待生产', value: '4' },
+    { label: '生产中', value: '5' },
+    { label: '已完成', value: '6' },
+    { label: '已延期', value: '7' }
+  ]);
   const EXEC_TYPE_OPTIONS = Object.freeze([
     { label: '自制', value: EXEC_TYPE.HOMEMADE },
     { label: '请托', value: EXEC_TYPE.ENTRUST },
@@ -360,6 +476,7 @@
     data() {
       return {
         visible: false,
+        entryReadonly: false,
         form: {},
         currentPlan: null,
         planRoutingPayload: null,
@@ -367,13 +484,28 @@
         tableData: [],
         calendarSourceData: [],
         planDataList: [],
+        allPlanDataList: [],
         selection: [],
+        planPickerVisible: false,
+        planPickerTitle: '添加计划',
+        planPickerLoading: false,
+        planPickerTimeDimensionPlanType: '',
+        planPickerWhere: {
+          code: '',
+          productCode: '',
+          status: '',
+          batchNo: ''
+        },
+        planPickerSelection: [],
+        planPickerSelectionAll: [],
+        planPickerSelectionSyncing: false,
         dialogRenderKey: 0,
         dotLineLoading: false,
         dotLineFullscreen: false,
         teamOptions: [],
         factoryList: [],
         executionTypeOptions: EXEC_TYPE_OPTIONS,
+        planPickerStatusOptions: PLAN_PICKER_STATUS_OPTIONS,
         EXEC_TYPE,
         columns: [
           {
@@ -607,6 +739,9 @@
           .filter((item) => item !== null && item !== undefined && item !== '')
           .join('');
         return text || '—';
+      },
+      isCurrentPlanReadonly() {
+        return this.entryReadonly || this.isReadonlyPlan(this.currentPlan);
       }
     },
     created() {
@@ -614,10 +749,203 @@
       this.loadFactoryOptions();
     },
     methods: {
+      getPlanRowKey(row) {
+        return String(row?.id ?? row?.planId ?? '');
+      },
+      isReadonlyPlan(row) {
+        return READONLY_LAYOUT_STATUSES.includes(String(row?.status ?? ''));
+      },
+      async loadPlansByIds(ids = []) {
+        const uniqueIds = Array.from(
+          new Set(
+            (ids || []).filter(
+              (id) => id !== null && id !== undefined && id !== ''
+            )
+          )
+        );
+        if (!uniqueIds.length) {
+          return [];
+        }
+        const data = await getList({
+          pageNum: 1,
+          size: uniqueIds.length,
+          ids: uniqueIds
+        });
+        const orderMap = new Map(
+          uniqueIds.map((id, index) => [String(id), index])
+        );
+        const list = Array.isArray(data?.list) ? data.list : [];
+        return [...list].sort((a, b) => {
+          const aIndex = orderMap.get(String(a?.id ?? ''));
+          const bIndex = orderMap.get(String(b?.id ?? ''));
+          return (
+            (aIndex ?? Number.MAX_SAFE_INTEGER) -
+            (bIndex ?? Number.MAX_SAFE_INTEGER)
+          );
+        });
+      },
+      buildPlanTableRows(selectedRows = [], sourceRows = []) {
+        const sourceList =
+          Array.isArray(sourceRows) && sourceRows.length
+            ? sourceRows
+            : selectedRows;
+        const selectedKeySet = new Set(
+          (selectedRows || [])
+            .map((item) => this.getPlanRowKey(item))
+            .filter((item) => item !== '')
+        );
+        return sourceList
+          .filter((item) => selectedKeySet.has(this.getPlanRowKey(item)))
+          .map((item, index) => ({
+            ...deepClone(item),
+            serialNumber: index + 1
+          }));
+      },
+      syncTableSelection(refName, rows) {
+        this.$nextTick(() => {
+          const table = this.$refs[refName];
+          if (!table) {
+            return;
+          }
+          if (refName === 'planPickerTable') {
+            this.planPickerSelectionSyncing = true;
+          }
+          setTimeout(() => {
+            if (typeof table?.setSelectedRows === 'function') {
+              table.setSelectedRows(rows || []);
+            } else {
+              table?.clearSelection?.();
+              (rows || []).forEach((row) => {
+                table?.toggleRowSelection?.(row, true);
+              });
+            }
+            if (refName === 'planPickerTable') {
+              this.$nextTick(() => {
+                this.planPickerSelectionSyncing = false;
+              });
+            }
+          }, 0);
+        });
+      },
+      async planPickerDatasource({ page, limit, where }) {
+        this.planPickerLoading = true;
+        try {
+          const requestWhere = { ...(where || {}) };
+          if (requestWhere.status) {
+            requestWhere.status = [requestWhere.status];
+          }
+          const data = await getList({
+            ...requestWhere,
+            pageNum: page,
+            size: limit,
+            timeDimensionPlanType: this.planPickerTimeDimensionPlanType
+          });
+          this.allPlanDataList = Array.isArray(data?.list) ? data.list : [];
+          const selectedMap = new Map(
+            (this.planPickerSelectionAll || []).map((item) => [
+              this.getPlanRowKey(item),
+              item
+            ])
+          );
+          this.planPickerSelection = this.allPlanDataList.filter((item) =>
+            selectedMap.has(this.getPlanRowKey(item))
+          );
+          this.syncTableSelection('planPickerTable', this.planPickerSelection);
+          return data;
+        } finally {
+          this.planPickerLoading = false;
+        }
+      },
+      async openPlanPicker(timeDimensionPlanType, title) {
+        if (this.entryReadonly) {
+          return;
+        }
+        this.planPickerTitle = title || '添加计划';
+        this.planPickerTimeDimensionPlanType = timeDimensionPlanType;
+        this.planPickerWhere = {
+          code: '',
+          productCode: '',
+          status: '',
+          batchNo: ''
+        };
+        this.planPickerSelectionAll = this.buildPlanTableRows(
+          this.planDataList
+        );
+        this.planPickerSelection = [];
+        this.allPlanDataList = [];
+        this.planPickerVisible = true;
+        this.$nextTick(() => {
+          this.$refs.planPickerTable?.reload({ page: 1, where: {} });
+        });
+      },
+      handlePlanPickerSearch() {
+        this.$refs.planPickerTable?.reload({
+          page: 1,
+          where: { ...this.planPickerWhere }
+        });
+      },
+      resetPlanPickerSearch() {
+        this.planPickerWhere = {
+          code: '',
+          productCode: '',
+          status: '',
+          batchNo: ''
+        };
+        this.$refs.planPickerTable?.reload({ page: 1, where: {} });
+      },
+      handlePlanPickerSelectionChange(rows) {
+        if (this.planPickerSelectionSyncing) {
+          return;
+        }
+        const currentPageSelection = Array.isArray(rows) ? rows : [];
+        const currentPageKeySet = new Set(
+          (this.allPlanDataList || [])
+            .map((item) => this.getPlanRowKey(item))
+            .filter((item) => item !== '')
+        );
+        const selectedMap = new Map(
+          (this.planPickerSelectionAll || [])
+            .filter((item) => !currentPageKeySet.has(this.getPlanRowKey(item)))
+            .map((item) => [this.getPlanRowKey(item), item])
+        );
+        currentPageSelection.forEach((item) => {
+          selectedMap.set(this.getPlanRowKey(item), item);
+        });
+        this.planPickerSelection = currentPageSelection;
+        this.planPickerSelectionAll = Array.from(selectedMap.values());
+      },
+      async confirmPlanPicker() {
+        const nextPlanDataList = this.buildPlanTableRows(
+          this.planPickerSelectionAll
+        );
+        if (!nextPlanDataList.length) {
+          this.$message.warning('请至少选择一条计划');
+          return;
+        }
+        const currentPlanKey = this.getPlanRowKey(this.currentPlan);
+        const matchedCurrentPlan = nextPlanDataList.find(
+          (item) => this.getPlanRowKey(item) === currentPlanKey
+        );
+        this.planPickerVisible = false;
+        this.planDataList = nextPlanDataList;
+        this.currentPlan = null;
+        this.planRoutingPayload = null;
+        this.taskList = [];
+        this.selection = [];
+        this.dotLineLoading = false;
+        await this.refreshSchedulingView();
+        this.selection = matchedCurrentPlan ? [matchedCurrentPlan] : [];
+        this.syncTableSelection(
+          'planTable',
+          matchedCurrentPlan ? [matchedCurrentPlan] : []
+        );
+      },
       handlePlanSelectionChange(rows) {
         const selectedRows = Array.isArray(rows) ? rows : [];
         const currentPlan =
-          selectedRows.length > 0 ? selectedRows[selectedRows.length - 1] : null;
+          selectedRows.length > 0
+            ? selectedRows[selectedRows.length - 1]
+            : null;
         if (selectedRows.length > 1 && currentPlan) {
           this.selection = [currentPlan];
           this.$nextTick(() => {
@@ -770,9 +1098,15 @@
         this.$set(row, 'executionFactoryName', '');
       },
       onExecutionTypeChange(row) {
+        if (this.isCurrentPlanReadonly) {
+          return;
+        }
         this.clearExecutionTeamFields(row);
       },
       onExecutionObjectChange(row) {
+        if (this.isCurrentPlanReadonly) {
+          return;
+        }
         const t = this.execTypeNum(row);
         if (t === EXEC_TYPE.HOMEMADE) {
           const team = this.teamOptions.find(
@@ -826,6 +1160,9 @@
         }
       },
       handleTimeChange(row, changeKey) {
+        if (this.isCurrentPlanReadonly) {
+          return;
+        }
         if (isEndBeforeStart(row.executionStartTime, row.executionEndTime)) {
           this.$message.warning('执行结束时间不能小于执行开始时间');
           this.$set(row, changeKey, '');
@@ -946,7 +1283,8 @@
           }
 
           const deadline =
-            this.currentPlan?.planDeliveryTime || this.currentPlan?.deliveryTime;
+            this.currentPlan?.planDeliveryTime ||
+            this.currentPlan?.deliveryTime;
           if (deadline) {
             const d = new Date(deadline).getTime();
             const checkStart = (val, name) => {
@@ -984,7 +1322,8 @@
             executionType: execType,
             planCode: row.planCode || plan?.code,
             planId: row.planId ?? plan?.id,
-            routingId: row.routingId ?? plan?.produceRoutingId ?? head.routingId,
+            routingId:
+              row.routingId ?? plan?.produceRoutingId ?? head.routingId,
             routingName:
               row.routingName || plan?.produceRoutingName || head.routingName,
             sourceTaskId: row.sourceTaskId ?? row.taskId,
@@ -1034,12 +1373,41 @@
         this.tableData = this.initGanttFlatRows(data || []);
       },
       async open(row) {
+        const ids = Array.isArray(row?.ids) ? row.ids : [];
+        const readonly = !!row?.readonly;
+        const loading = ids.length
+          ? this.$loading({ lock: true, text: '加载计划中...' })
+          : null;
+        let selectedPlans = Array.isArray(row)
+          ? row
+          : Array.isArray(row?.selectedPlans)
+          ? row.selectedPlans
+          : [];
+        let allPlans = Array.isArray(row?.allPlans)
+          ? row.allPlans
+          : selectedPlans;
+        try {
+          if (ids.length) {
+            const planList = await this.loadPlansByIds(ids);
+            selectedPlans = planList;
+            allPlans = planList;
+          }
+        } finally {
+          loading?.close();
+        }
         this.dialogRenderKey += 1;
-        this.planDataList = row;
+        this.entryReadonly = readonly;
+        this.allPlanDataList = deepClone(allPlans || []);
+        this.planDataList = this.buildPlanTableRows(
+          selectedPlans,
+          this.allPlanDataList
+        );
         this.currentPlan = null;
         this.planRoutingPayload = null;
         this.taskList = [];
         this.selection = [];
+        this.planPickerVisible = false;
+        this.planPickerSelection = [];
         this.dotLineLoading = false;
         this.dotLineFullscreen = false;
         this.calendarSourceData = [];
@@ -1264,6 +1632,10 @@
           this.$message.warning('请先勾选一条计划后再确定');
           return;
         }
+        if (this.isCurrentPlanReadonly) {
+          this.$message.warning('当前计划状态下布局数据仅支持查看');
+          return;
+        }
         if (!this.taskList.length) {
           this.$message.warning('当前没有可提交的工艺配置');
           return;
@@ -1286,10 +1658,14 @@
       },
       cancel() {
         this.form = {};
+        this.entryReadonly = false;
         this.currentPlan = null;
         this.planRoutingPayload = null;
         this.taskList = [];
         this.selection = [];
+        this.planPickerSelection = [];
+        this.planPickerSelectionAll = [];
+        this.planPickerTimeDimensionPlanType = '';
         this.dotLineLoading = false;
         this.dotLineFullscreen = false;
         this.visible = false;
@@ -1303,9 +1679,63 @@
     padding: 20px 0;
   }
 
+  .btnList {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 12px;
+  }
+
+  .plan-picker-search {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    gap: 12px;
+    margin-bottom: 12px;
+  }
+
+  .plan-picker-search__item {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    :deep(.el-input),
+    :deep(.el-select) {
+      width: 180px;
+    }
+  }
+
+  .plan-picker-search__label {
+    color: #606266;
+    font-size: 12px;
+    white-space: nowrap;
+  }
+
+  .plan-picker-search__actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+
+  .scheduling-content {
+    display: flex;
+    align-items: flex-end;
+    gap: 12px;
+  }
+
+  .form-wrapper {
+    width: calc(100% - 700px);
+    min-width: 0;
+  }
+
   .plan-dot-line-side {
-    height: 400px;
+    width: 700px;
+    flex-shrink: 0;
+    height: 525px;
     overflow: hidden;
+    padding-left: 12px;
+    box-sizing: border-box;
+    display: flex;
+    flex-direction: column;
   }
 
   .plan-dot-line-side.is-fullscreen {
@@ -1320,17 +1750,28 @@
     background: #fff;
     border-radius: 8px;
     box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
+    padding: 20px;
   }
 
-  .plan-dot-line-side .pane-box {
+  .plan-dot-line-detail {
+    display: flex;
+    flex-direction: column;
     height: 100%;
+  }
+
+  .plan-dot-line-detail .plan-time-bar {
+    flex-shrink: 0;
+  }
+
+  .plan-dot-line-detail .plan-dot-line-scrollable {
+    flex: 1;
     overflow: auto;
-    padding-left: 12px;
-    padding-right: 0;
+    padding-bottom: 20px;
     box-sizing: border-box;
+    min-height: 0;
   }
 
-  .plan-dot-line-detail .plan-dot-line,
+  .plan-dot-line-detail .plan-dot-line-scrollable,
   .plan-dot-line-detail .top-route,
   .plan-dot-line-detail .config-panel {
     width: 100%;
@@ -1339,10 +1780,6 @@
     box-sizing: border-box;
   }
 
-  .plan-dot-line-detail .plan-dot-line {
-    min-height: 360px;
-  }
-
   .plan-dot-line-detail .top-route,
   .plan-dot-line-detail .config-panel {
     border: 1px solid #ebeef5;
@@ -1383,6 +1820,11 @@
     gap: 8px;
   }
 
+  .readonly-tip {
+    color: #e6a23c;
+    font-size: 12px;
+  }
+
   .plan-dot-line-detail .route-steps {
     width: 100%;
     overflow-x: auto;
@@ -1534,8 +1976,12 @@
   }
 
   .btnList {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin: 10px 0 12px;
     button {
-      margin: 10px;
+      margin: 0;
     }
   }
 </style>

+ 27 - 21
src/views/productionPlan/index.vue

@@ -582,6 +582,7 @@
         ],
         columns: [],
         selection: [],
+        allPlanTableData: [],
         clientEnvironmentId: '',
         // factoryShow: false,
         factoryType: 3,
@@ -645,6 +646,18 @@
       this.getplannedReleaseRequire('planned_release_require');
     },
     methods: {
+      flattenPlanRows(list = []) {
+        const result = [];
+        (list || []).forEach((item) => {
+          const childList = Array.isArray(item?.childList) ? item.childList : [];
+          if (childList.length > 0 && item.splitBatch != 2) {
+            result.push(...childList);
+          } else if (item) {
+            result.push(item);
+          }
+        });
+        return result;
+      },
       canApprove(row) {
         const canRelease =
           row.status == 2 &&
@@ -1319,15 +1332,17 @@
       },
       /* 数据转为树形结构 */
       parseData(data) {
+        const treeList = this.$util.toTreeData({
+          data: data.list,
+          count: data.total,
+
+          idField: 'code',
+          parentIdField: 'joinPlanCode'
+        });
+        this.allPlanTableData = this.flattenPlanRows(treeList);
         return {
           ...data,
-          list: this.$util.toTreeData({
-            data: data.list,
-            count: data.total,
-
-            idField: 'code',
-            parentIdField: 'joinPlanCode'
-          })
+          list: treeList
         };
       },
 
@@ -1350,19 +1365,7 @@
       },
       handleSelectionChange(list) {
         console.log(list, 'list ___');
-        if (list.length > 0) {
-          let _list = [];
-          list.forEach((e) => {
-            if (e.childList.length > 0 && e.splitBatch != 2) {
-              _list.push(...e.childList);
-            } else {
-              _list.push(e);
-            }
-          });
-          this.selection = _list;
-        } else {
-          this.selection = [];
-        }
+        this.selection = this.flattenPlanRows(list);
       },
 
       process(row) {
@@ -1395,7 +1398,10 @@
 
       productionSchedulingSuccess(data) {
         // data.productionPlanIds = this.selection.map((i) => i.id);
-        this.$refs.newFactoryProductionSchedulingRef.open(this.selection);
+        this.$refs.newFactoryProductionSchedulingRef.open({
+          selectedPlans: this.selection,
+          allPlans: this.allPlanTableData
+        });
       },
 
       //生产前准备

+ 36 - 1
src/views/workOrder/index.vue

@@ -16,6 +16,14 @@
           >批量派单
           <!--  -->
         </el-button>
+        <el-button
+          class="ele-btn-icon"
+          size="small"
+          type="primary"
+          v-if="$hasPermission('aps:productionplan:scheduling‌')"
+          @click="openFactoryScheduling"
+          >工厂排产
+        </el-button>
       </div>
 
       <el-tabs v-model="tabValue" type="card" @tab-click="handleTabClick">
@@ -233,6 +241,7 @@
       type="order"
       @update="reload"
     />
+    <newFactoryProductionScheduling ref="newFactoryProductionSchedulingRef" />
   </div>
 </template>
 
@@ -260,6 +269,7 @@
   import Details from './components/details.vue';
   import AssetsDialog from './components/AssetsDialog.vue';
   import checkProductionPreparations from '@/views/productionPlan/components/checkProductionPreparations.vue';
+  import newFactoryProductionScheduling from '@/views/productionPlan/components/newFactoryProductionScheduling.vue';
   import { getProductPlanDetail } from '@/api/productionPlan/index';
   import { parameterGetByCode } from '@/api/mainData/index';
   import planDotLinReleaseDialog from './components/planDotLinReleaseDialog.vue';
@@ -275,7 +285,8 @@
       Details,
       AssetsDialog,
       checkProductionPreparations,
-      planDotLinReleaseDialog
+      planDotLinReleaseDialog,
+      newFactoryProductionScheduling
     },
     data() {
       return {
@@ -776,6 +787,7 @@
             this.selectionMap[row.id] = {
               id: row.id,
               firstTaskId: row.firstTaskId,
+              productionPlanId: row.productionPlanId,
               productName: row.productName,
               productCode: row.productCode,
               code: row.code,
@@ -793,6 +805,7 @@
           this.selectionMap[row.id] = {
             id: row.id,
             firstTaskId: row.firstTaskId,
+            productionPlanId: row.productionPlanId,
             productName: row.productName,
             productCode: row.productCode,
             code: row.code,
@@ -824,6 +837,28 @@
 
         this.$refs.batchRef.open(list);
       },
+      openFactoryScheduling() {
+        const list = Object.values(this.selectionMap);
+        if (list.length === 0) {
+          this.$message.warning('请至少选择一条工厂排产的数据');
+          return;
+        }
+        const ids = Array.from(
+          new Set(
+            list
+              .map((item) => item.productionPlanId)
+              .filter((id) => id !== null && id !== undefined && id !== '')
+          )
+        );
+        if (ids.length === 0) {
+          this.$message.warning('所选数据未关联生产计划,无法打开工厂排产');
+          return;
+        }
+        this.$refs.newFactoryProductionSchedulingRef.open({
+          ids,
+          readonly: true
+        });
+      },
 
       // 删除拆单数据
       remove(row) {