<template>
  <ele-modal
    :key="dialogRenderKey"
    width="100%"
    :visible.sync="visible"
    :close-on-click-modal="false"
    custom-class="ele-dialog-form scheduling-dialog"
    :title="schedulingTitle"
    :maxable="true"
  >
    <projectGantt
      :style="{ height: ganttHeight }"
      :ganttTasks="tableData"
      :planList="planDataList"
      :calendarSourceData="calendarSourceData"
      :defaultTab="defaultGanttTab"
      :resource-tabs-config="resourceTabsConfig"
      :resource-label-mode="schedulingMode"
      :dateRange="dateRange"
      @tab-change="handleTabChange"
    >
      <template v-slot:toolbar>
        <el-date-picker
          v-model="dateRange"
          type="daterange"
          size="small"
          value-format="yyyy-MM-dd"
          range-separator="至"
          start-placeholder="计划开始时间"
          end-placeholder="计划结束时间"
          @input="handleDateInput"
          @change="handleDateChange"
          :append-to-body="false"
        ></el-date-picker>
      </template>
    </projectGantt>
    <ele-split-layout
      class="scheduling-content"
      space="0px"
      :width="splitPaneWidth"
      :resizable="true"
      :min-size="splitPaneMinSize"
      :max-size="splitPaneMaxSize"
      :left-style="{
        overflow: 'visible',
        width: '100%'
      }"
      :right-style="{
        overflow: 'hidden'
      }"
      :responsive="isCompactSchedulingLayout"
      :style="{ height: schedulingContentHeight }"
    >
      <div
        :class="[
          'form-wrapper',
          {
            'is-order-scheduling': isOrderScheduling,
            'is-plan-scheduling': !isOrderScheduling
          }
        ]"
      >
        <div class="plan-table-status-tabs">
          <el-tabs
            v-model="planTableStatusTab"
            @tab-click="handlePlanTableStatusTabClick"
          >
            <el-tab-pane
              v-for="item in planTableStatusTabs"
              :key="item.name"
              :name="item.name"
              :label="item.label"
            />
          </el-tabs>
        </div>
        <ele-pro-table
          :key="planTableCacheKey"
          width="100%"
          ref="planTable"
          :columns="columns"
          :cache-key="planTableCacheKey"
          :datasource="planDataList"
          row-key="id"
          :selection.sync="selection"
          :height="planTableHeight"
          :page-size="20"
          :pagination-class="planPaginationClass"
          @reload="handlePlanTableReload"
          @update:selection="handlePlanSelectionChange"
        >
          <template v-slot:action="{ row }">
            <el-link
              type="primary"
              :underline="false"
              @click="handlePublish(row)"
            >
              {{ getPlanActionText(row) }}
            </el-link>
          </template>
          <template v-slot:dotLineStatus="{ row }">
            <el-tag type="success" size="mini" v-if="row.dotLineStatus == 1">
              已布点
            </el-tag>
            <el-tag type="danger" size="mini" v-if="row.dotLineStatus == 0">
              未布点
            </el-tag>
          </template>
        </ele-pro-table>
      </div>

      <template v-slot:content>
        <div
          v-loading="dotLineLoading"
          :class="[
            'plan-dot-line-side',
            {
              'is-fullscreen': dotLineFullscreen,
              'is-plan-scheduling': !isOrderScheduling
            }
          ]"
        >
          <div class="plan-dot-line-detail">
            <plan-route-panel
              v-if="!isOrderScheduling"
              :plan-start-display="planStartDisplay"
              :plan-end-display="planEndDisplay"
              :plan-num-display="planNumDisplay"
              :task-list="taskList"
              :route-steps-active="routeStepsActive"
              :route-des-index="routeDesIndex"
              :route-step-title="routeStepTitle"
              :compact="isOrderScheduling"
            />
            <div
              :class="[
                'plan-dot-line-scrollable',
                {
                  'is-order-dispatch-scroll': isOrderScheduling,
                  'is-plan-config-scroll': !isOrderScheduling
                }
              ]"
            >
              <task-config-panel
                :key="taskConfigPanelRenderKey"
                :class="{ 'factory-task-config-panel': !isOrderScheduling }"
                :dot-line-fullscreen="dotLineFullscreen"
                :is-current-plan-readonly="isCurrentPlanReadonly"
                :order-detail-readonly="isOrderDetailReadonly"
                :current-plan="currentPlan"
                :dot-line-loading="dotLineLoading"
                :task-list="taskList"
                :execution-type-options="executionTypeOptions"
                :team-options="teamOptions"
                :get-homemade-options="getHomemadeOptions"
                :homemade-object-label="homemadeObjectLabel"
                :show-quantity-column="isOrderScheduling"
                :order-dispatch-style="isOrderScheduling"
                :show-approve-button="!isOrderScheduling"
                :approve-button-disabled="!canApproveCurrentPlan"
                :approve-loading="approvalLoading"
                :table-max-height="taskConfigTableMaxHeight"
                :fill-table-height="!isOrderScheduling"
                :factory-list="factoryList"
                :exec-type="EXEC_TYPE"
                :task-name="taskName"
                :exec-type-num="execTypeNum"
                :is-execution-end-over-delivery="isExecutionEndOverDelivery"
                :is-execution-end-over-plan-end="isExecutionEndOverPlanEnd"
                @toggle-fullscreen="toggleDotLineFullscreen"
                @submit="submitDotLineConfig"
                @execution-type-change="onExecutionTypeChange"
                @execution-object-change="onExecutionObjectChange"
                @parallelism-change="handleParallelismChange"
                @time-change="handleTimeChange"
                @add-task-row="addTaskRow"
                @remove-task-row="removeTaskRow"
                @add-dispatch-stations="addDispatchStations"
                @remove-dispatch-station="removeDispatchStation"
                @dispatch-action-success="handleDispatchActionSuccess"
                @row-submit="submitDotLineRow"
                @approve="handleApproveCurrentPlan"
              />
            </div>
          </div>
        </div>
      </template>
    </ele-split-layout>

    <div slot="footer">
      <!-- <el-button plain @click="cancel">取消</el-button> -->
      <el-button @click="cancel">关闭</el-button>
    </div>

    <plan-picker-dialog
      ref="planPickerTable"
      :title="planPickerTitle"
      :visible.sync="planPickerVisible"
      :where.sync="planPickerWhere"
      :status-options="planPickerStatusOptions"
      :columns="columns"
      :loading="planPickerLoading"
      :datasource="planPickerDatasource"
      :selection.sync="planPickerSelection"
      @search="handlePlanPickerSearch"
      @reset-search="resetPlanPickerSearch"
      @confirm="confirmPlanPicker"
      @update:selection="handlePlanPickerSelectionChange"
    />
  </ele-modal>
</template>

<script>
  import projectGantt from './gantt/project-gantt.vue';
  import PlanPickerDialog from './newFactoryProductionScheduling/PlanPickerDialog.vue';
  import PlanRoutePanel from './newFactoryProductionScheduling/PlanRoutePanel.vue';
  import TaskConfigPanel from './newFactoryProductionScheduling/TaskConfigPanel.vue';
  import {
    getList,
    teamSchedulingGanttChart,
    workstationSchedulingGanttChart,
    factorySchedulingGanttChart,
    getPlansByIds
  } from '@/api/productionPlan/index.js';
  import { getTaskListById } from '@/api/materialPlan';
  import {
    listByFirstTaskId,
    parameterGetByCode,
    teamPage
  } from '@/api/mainData';
  import { getFactoryarea } from '@/api/saleOrder';
  import { getUserPage } from '@/api/system/organization';
  import { release, releaseByApproval } from '@/api/productionPlan/order.js';
  import {
    getPlanDotLine,
    savePlanDotLine
  } from '@/api/productionPlan/planDotLine';
  import { deepClone } from '@/utils';
  import {
    EXEC_TYPE,
    EXEC_TYPE_OPTIONS,
    PLAN_PICKER_STATUS_OPTIONS,
    READONLY_LAYOUT_STATUSES
  } from './newFactoryProductionScheduling.constants';
  import {
    createSchedulingColumns,
    createWorkOrderSchedulingColumns
  } from './newFactoryProductionScheduling.columns';
  import {
    createPlanPickerWhere,
    formatDotLineDateTime,
    isEndAfterPlanEnd,
    isEndBeforeStart,
    toSafeNumber
  } from './newFactoryProductionScheduling.utils';

  export default {
    components: {
      projectGantt,
      PlanPickerDialog,
      PlanRoutePanel,
      TaskConfigPanel
    },
    data() {
      const now = new Date();
      const startDate = `${now.getFullYear()}-${String(
        now.getMonth() + 1
      ).padStart(2, '0')}-01`;
      const lastDay = new Date(
        now.getFullYear(),
        now.getMonth() + 1,
        0
      ).getDate();
      const endDate = `${now.getFullYear()}-${String(
        now.getMonth() + 1
      ).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
      return {
        visible: false,
        entryReadonly: false,
        form: {},
        currentPlan: null,
        planRoutingPayload: null,
        taskList: [],
        tableData: [],
        calendarSourceData: [],
        planDataList: [],
        allPlanDataList: [],
        selection: [],
        activeTab: 'factory',
        dateRange: [startDate, endDate],
        planPickerVisible: false,
        planPickerTitle: '添加计划',
        planPickerLoading: false,
        planPickerTimeDimensionPlanType: '',
        planPickerWhere: createPlanPickerWhere(),
        planPickerSelection: [],
        planPickerSelectionAll: [],
        planPickerSelectionSyncing: false,
        planTableSelectionSyncing: false,
        planTableStatusTab: 'unpublished',
        dialogRenderKey: 0,
        dotLineLoading: false,
        dotLineFullscreen: false,
        approvalLoading: false,
        isReview: false,
        orderDetailReadonly: false,
        orderDetailActionMode: '',
        teamOptions: [],
        workstationOptionsMap: {},
        workstationOptionsLoadingMap: {},
        factoryList: [],
        executionTypeOptions: EXEC_TYPE_OPTIONS,
        planPickerStatusOptions: PLAN_PICKER_STATUS_OPTIONS,
        EXEC_TYPE,
        schedulingMode: 'plan',
        selectedWorkOrders: [],
        orderListLoader: null,
        orderDateReloadTimer: null,
        columns: createSchedulingColumns(),
        viewportWidth: typeof window === 'undefined' ? 1920 : window.innerWidth,
        viewportHeight: typeof window === 'undefined' ? 900 : window.innerHeight
      };
    },
    computed: {
      routeStepsActive() {
        const list = this.taskList;
        if (!list.length) return 0;
        for (let i = 0; i < list.length; i++) {
          if (!this.isTaskDone(list[i])) return i;
        }
        return list.length;
      },
      routeDesIndex() {
        const list = this.taskList;
        if (!list.length) return -1;
        const a = this.routeStepsActive;
        return a >= list.length ? list.length - 1 : a;
      },
      planStartDisplay() {
        const t = this.currentPlan?.startTime;
        return t ? formatDotLineDateTime(t) || String(t) : '—';
      },
      planEndDisplay() {
        const t = this.currentPlan?.endTime;
        return t ? formatDotLineDateTime(t) || String(t) : '—';
      },
      planNumDisplay() {
        const num = this.currentPlan?.productNum;
        const unit = this.currentPlan?.measuringUnit || this.currentPlan?.unit;
        const text = [num, unit]
          .filter((item) => item !== null && item !== undefined && item !== '')
          .join('');
        return text || '—';
      },
      isCurrentPlanReadonly() {
        return (
          (!this.isOrderScheduling && this.entryReadonly) ||
          this.isOrderDetailReadonly ||
          (!this.isOrderScheduling && this.isReadonlyPlan(this.currentPlan))
        );
      },
      isOrderDetailReadonly() {
        return (
          this.isOrderScheduling &&
          !!this.currentPlan &&
          this.orderDetailActionMode === 'view'
        );
      },
      // 当前弹窗同时承载两条业务链路：
      // 1. 工厂排程：从生产计划页面进入，不传 mode，保持默认 plan 模式。
      // 2. 班组排程：从工单页面进入，显式传入 mode = order。
      // 后续表格列、资源页签、执行对象和保存字段都以该判断隔离。
      isOrderScheduling() {
        return this.schedulingMode === 'order';
      },
      resourceTabsConfig() {
        if (this.isOrderScheduling) {
          return [
            { key: 'team', label: '工位' },
            { key: 'line', label: '设备' },
            { key: 'person', label: '人员' }
          ];
        }
        return undefined;
      },
      homemadeObjectLabel() {
        return this.isOrderScheduling ? '执行对象（人员/工位）' : '执行对象';
      },
      defaultGanttTab() {
        return this.isOrderScheduling ? 'team' : 'factory';
      },
      schedulingTitle() {
        return this.isOrderScheduling ? '班组排程' : '工厂排产';
      },
      taskConfigPanelRenderKey() {
        const planKey =
          this.currentPlan?.workOrderId || this.currentPlan?.id || 'empty';
        return [
          this.schedulingMode,
          this.planTableStatusTab,
          planKey,
          this.orderDetailActionMode || 'none',
          this.isCurrentPlanReadonly ? 'readonly' : 'edit'
        ].join('-');
      },
      planTableCacheKey() {
        return this.isOrderScheduling
          ? 'aps-team-scheduling-work-order-table'
          : 'aps-factory-scheduling-plan-table';
      },
      planTableStatusTabs() {
        if (this.isOrderScheduling) {
          return [
            {
              name: 'undispatched',
              label: '未派单'
            },
            {
              name: 'dispatched',
              label: '已派单'
            }
          ];
        }
        return [
          {
            name: 'unpublished',
            label: '未发布'
          },
          {
            name: 'published',
            label: '已发布'
          }
        ];
      },
      isCompactSchedulingLayout() {
        return this.viewportWidth <= 1180;
      },
      splitPaneWidth() {
        if (this.isOrderScheduling) {
          if (this.viewportWidth <= 1280) return '36%';
          if (this.viewportWidth <= 1440) return '39%';
          if (this.viewportWidth <= 1680) return '42%';
          return '44%';
        }
        if (this.viewportWidth <= 1280) return '44%';
        if (this.viewportWidth <= 1440) return '50%';
        return '58%';
      },
      splitPaneMaxSize() {
        if (this.isOrderScheduling) {
          if (this.viewportWidth <= 1280) return -620;
          if (this.viewportWidth <= 1440) return -680;
          return -720;
        }
        return this.viewportWidth <= 1440 ? -520 : -420;
      },
      splitPaneMinSize() {
        if (this.viewportWidth <= 1280) return 280;
        if (this.viewportWidth <= 1440) return 320;
        return 360;
      },
      schedulingLayoutAvailableHeightValue() {
        const headerHeight = this.viewportHeight <= 760 ? 38 : 42;
        const footerHeight = this.viewportHeight <= 760 ? 34 : 38;
        const bodyVerticalPadding = this.viewportHeight <= 760 ? 8 : 12;
        const contentGap = this.viewportHeight <= 760 ? 5 : 6;
        return (
          this.viewportHeight -
          10 -
          headerHeight -
          footerHeight -
          bodyVerticalPadding -
          contentGap
        );
      },
      ganttPanelHeightValue() {
        const minGanttHeight =
          this.viewportHeight <= 720
            ? 230
            : this.viewportHeight <= 820
            ? 300
            : this.viewportHeight <= 960
            ? 340
            : 380;
        const maxGanttHeight =
          this.viewportHeight <= 820
            ? 390
            : this.viewportHeight <= 1080
            ? 500
            : 560;
        const balancedHeight = Math.floor(
          this.schedulingLayoutAvailableHeightValue / 2
        );
        return Math.max(
          232,
          Math.min(maxGanttHeight, Math.max(minGanttHeight, balancedHeight))
        );
      },
      schedulingContentHeightValue() {
        const remainingHeight =
          this.schedulingLayoutAvailableHeightValue -
          this.ganttPanelHeightValue;
        const minHeight =
          this.viewportHeight <= 720
            ? 230
            : this.viewportHeight <= 820
            ? 300
            : this.viewportHeight <= 900
            ? 340
            : 380;
        return Math.max(minHeight, remainingHeight);
      },
      ganttHeight() {
        return `${this.ganttPanelHeightValue}px`;
      },
      schedulingContentHeight() {
        return `${this.schedulingContentHeightValue}px`;
      },
      planTableHeight() {
        const tabHeight = 42;
        const paginationHeight = 44;
        const tableHeight = Math.max(
          220,
          this.schedulingContentHeightValue - tabHeight - paginationHeight
        );
        if (tableHeight >= 0) {
          return `${tableHeight}px`;
        }
        if (this.isOrderScheduling) {
          // ele-pro-table 的 height 只作用在表格主体，工具栏和分页器会继续占用额外高度。
          // 班组排产右侧是自定义 flex 分页区，左侧需要略收表格主体高度，保证两个分页器垂直位置一致。
          if (this.viewportHeight <= 760) return `${360 - tabHeight}px`;
          if (this.viewportHeight <= 900) return `${420 - tabHeight}px`;
          return `${470 - tabHeight}px`;
        }
        if (this.viewportHeight <= 760) return `${360 - tabHeight}px`;
        if (this.viewportHeight <= 900) return `${420 - tabHeight}px`;
        return `${455 - tabHeight}px`;
      },
      planPaginationClass() {
        return this.isOrderScheduling
          ? 'ele-pagination-circle order-plan-pagination'
          : 'ele-pagination-circle plan-scheduling-pagination';
      },
      taskConfigTableMaxHeight() {
        if (this.isOrderScheduling) {
          return Math.max(260, this.schedulingContentHeightValue - 132);
        }
        return Math.max(280, this.schedulingContentHeightValue - 132);
      },
      canApproveCurrentPlan() {
        const row = this.currentPlan;
        const hasPermission =
          typeof this.$hasPermission !== 'function' ||
          this.$hasPermission('aps:productionplan:release');
        return (
          !this.isOrderScheduling &&
          !!row?.id &&
          this.planTableStatusTab === 'unpublished' &&
          String(row.status) === '2' &&
          String(row.dotLineStatus) === '1' &&
          row.splitBatch != 1 &&
          row.approvalStatus != 1 &&
          row.approvalStatus != 2 &&
          hasPermission
        );
      }
    },
    created() {
      this.loadTeamOptions();
      this.loadFactoryOptions();
      this.loadApprovalReviewConfig();
    },
    mounted() {
      this.updateViewportSize();
      window.addEventListener('resize', this.updateViewportSize);
    },
    beforeDestroy() {
      window.removeEventListener('resize', this.updateViewportSize);
    },
    methods: {
      updateViewportSize() {
        this.viewportWidth = window.innerWidth;
        this.viewportHeight = window.innerHeight;
      },
      normalizeId(value) {
        return value === null || value === undefined ? '' : String(value);
      },
      normalizeBackendLongId(value) {
        if (value === null || value === undefined || value === '') {
          return '';
        }
        const text = String(value).trim();
        return /^\d+$/.test(text) ? text : '';
      },
      normalizeBackendTextValue(value) {
        if (value === null || value === undefined || value === '') {
          return '';
        }
        const text = String(value).trim();
        return ['undefined', 'null'].includes(text.toLowerCase()) ? '' : text;
      },
      pickBackendTextValue(...values) {
        for (const value of values) {
          const text = this.normalizeBackendTextValue(value);
          if (text) {
            return text;
          }
        }
        return '';
      },
      normalizeOrderQuantity(value) {
        if (value === null || value === undefined || value === '') {
          return '';
        }
        const text = String(value).trim();
        return /^0+(\.0+)?$/.test(text) ? '' : value;
      },
      optionMatchesId(optionId, targetId) {
        return this.normalizeId(optionId) === this.normalizeId(targetId);
      },
      // Plan list helpers
      getPlanRowKey(row) {
        return String(row?.id ?? row?.planId ?? '');
      },
      isReadonlyPlan(row) {
        return READONLY_LAYOUT_STATUSES.includes(String(row?.status ?? ''));
      },
      getOrderStatusCode(row) {
        const status = row?.status;
        if (status && typeof status === 'object') {
          return String(status.code ?? status.value ?? status.id ?? '');
        }
        return String(status ?? '');
      },
      isOrderSchedulingReadonlyRow(row) {
        if (!this.isOrderScheduling) {
          return false;
        }
        return (
          this.planTableStatusTab === 'dispatched' ||
          ['4', '5', '6', '7'].includes(this.getOrderStatusCode(row))
        );
      },
      getPlanActionText(row) {
        if (this.isOrderSchedulingReadonlyRow(row)) {
          return '查看';
        }
        return !this.isOrderScheduling && this.isReadonlyPlan(row)
          ? '查看'
          : '布点';
      },
      getDefaultPlanTableStatusTab(mode = this.schedulingMode) {
        return mode === 'order' ? 'undispatched' : 'unpublished';
      },
      getPlanTableStatusRequestWhere(tabName = this.planTableStatusTab) {
        if (this.isOrderScheduling) {
          return tabName === 'dispatched'
            ? { statusList: [4, 5, 6, 7] }
            : { statusList: [8] };
        }
        return tabName === 'published'
          ? { status: ['4'] }
          : { status: ['2', '3'] };
      },
      getPlanTableStatusRequestWhereList(tabName = this.planTableStatusTab) {
        return [this.getPlanTableStatusRequestWhere(tabName)];
      },
      async handlePlanTableStatusTabClick() {
        this.orderDetailReadonly = false;
        this.orderDetailActionMode = '';
        this.currentPlan = null;
        this.planRoutingPayload = null;
        this.taskList = [];
        this.selection = [];
        this.syncTableSelection('planTable', []);
        await this.reloadPlanTableByStatus();
      },
      async reloadPlanTableByStatus() {
        if (this.isOrderScheduling) {
          await this.reloadOrderSchedulingOrdersByDateRange(this.dateRange);
        } else {
          await this.reloadPlanSchedulingPlansByStatus();
        }
      },
      async reloadPlanSchedulingPlansByStatus(options = {}) {
        const preserveCurrent = !!options.preserveCurrent;
        const currentPlanKey = preserveCurrent
          ? this.getPlanRowKey(this.currentPlan)
          : '';
        this.orderDetailReadonly = false;
        this.orderDetailActionMode = '';
        const loading = this.$loading({
          lock: true,
          text: '加载计划中...'
        });
        let matchedCurrentPlan = null;
        try {
          const requestWhereList = this.getPlanTableStatusRequestWhereList();
          const resultList = await Promise.all(
            requestWhereList.map((where) =>
              getList({
                pageNum: 1,
                size: -1,
                ...this.getSchedulingDateRangeParams(),
                ...where
              })
            )
          );
          const rows = resultList.reduce(
            (list, data) => list.concat(data?.list || []),
            []
          );
          this.allPlanDataList = deepClone(rows);
          this.planDataList = this.buildPlanTableRows(rows, rows);
          if (preserveCurrent && currentPlanKey) {
            matchedCurrentPlan =
              this.planDataList.find(
                (item) => this.getPlanRowKey(item) === currentPlanKey
              ) || null;
          }
        } catch (e) {
          this.$message.error(e.message || '获取计划数据失败');
        } finally {
          loading.close();
        }
        if (preserveCurrent && matchedCurrentPlan) {
          this.currentPlan = {
            ...(this.currentPlan || {}),
            ...matchedCurrentPlan
          };
          this.selection = [matchedCurrentPlan];
          this.syncTableSelection('planTable', [matchedCurrentPlan]);
          return;
        }
        this.selection = [];
        this.currentPlan = null;
        this.planRoutingPayload = null;
        this.taskList = [];
        this.syncTableSelection('planTable', []);
      },
      async handlePlanTableReload() {
        if (this.isOrderScheduling) {
          await this.reloadOrderSchedulingOrdersByDateRange(this.dateRange);
          return;
        }
        await this.refreshSchedulingView();
        await this.reloadPlanSchedulingPlansByStatus({ preserveCurrent: true });
        if (this.currentPlan?.id) {
          await this.loadPlanDotLineDetail(this.currentPlan);
        }
      },
      getSchedulingDateRangeParams() {
        if (!Array.isArray(this.dateRange) || this.dateRange.length !== 2) {
          return {};
        }
        if (!this.dateRange[0] || !this.dateRange[1]) {
          return {};
        }
        return {
          startDate: this.dateRange[0],
          endDate: this.dateRange[1]
        };
      },
      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
          }));
      },
      buildWorkOrderTableRows(rows = []) {
        return (rows || []).map((item, index) => ({
          ...deepClone(item),
          id: item.id ?? item.code ?? `work-order-${index}`,
          serialNumber: index + 1
        }));
      },
      syncTableSelection(refName, rows) {
        this.$nextTick(() => {
          const table = this.$refs[refName];
          if (!table) {
            return;
          }
          if (refName === 'planPickerTable') {
            this.planPickerSelectionSyncing = true;
          }
          if (refName === 'planTable') {
            this.planTableSelectionSyncing = 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;
              });
            }
            if (refName === 'planTable') {
              this.$nextTick(() => {
                this.planTableSelectionSyncing = false;
              });
            }
          }, 0);
        });
      },

      // Plan picker flow
      async planPickerDatasource({ page, limit, where }) {
        this.planPickerLoading = true;
        try {
          const requestWhere = { ...(where || {}) };
          requestWhere.status = Array.isArray(requestWhere.status)
            ? 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 = createPlanPickerWhere();
        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 = createPlanPickerWhere();
        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.orderDetailReadonly = false;
        this.orderDetailActionMode = '';
        this.currentPlan = null;
        this.planRoutingPayload = null;
        this.taskList = [];
        this.workstationOptionsMap = {};
        this.workstationOptionsLoadingMap = {};
        this.selection = [];
        this.dotLineLoading = false;
        await this.refreshSchedulingView();
        this.selection = matchedCurrentPlan ? [matchedCurrentPlan] : [];
        this.syncTableSelection(
          'planTable',
          matchedCurrentPlan ? [matchedCurrentPlan] : []
        );
      },

      // Selected plan detail
      handlePlanSelectionChange(rows) {
        if (this.planTableSelectionSyncing) {
          return;
        }
        const nextRows = Array.isArray(rows) ? rows : [];
        const row = nextRows[nextRows.length - 1];
        if (!row) {
          this.selection = [];
          this.syncTableSelection('planTable', []);
          return;
        }
        this.handlePublish(row);
      },
      resolveCurrentPlan(row) {
        if (!this.isOrderScheduling) {
          return row;
        }
        return {
          ...row,
          workOrderId: row.id,
          id: row.productionPlanId || row.id,
          code: row.productionPlanCode || row.code,
          productionOrderNumber: row.code,
          startTime: row.planStartTime || row.startTime,
          endTime: row.planCompleteTime || row.endTime,
          productNum: row.formingNum ?? row.productNum,
          unit: row.measuringUnit || row.unit
        };
      },
      handlePublish(row) {
        const skipBomCheck =
          this.isOrderScheduling && String(row?.workOrderType ?? '') == '3';
        if (!skipBomCheck && !row.bomCategoryId) {
          this.$message.warning('该计划没有配置BOM请先配置BOM');
          return;
        }
        const readonly = this.isOrderSchedulingReadonlyRow(row);
        this.orderDetailReadonly = readonly;
        this.orderDetailActionMode = readonly ? 'view' : 'edit';
        const currentPlan = this.resolveCurrentPlan(row);
        currentPlan._orderDetailReadonly = readonly;
        currentPlan.orderDetailReadonly = readonly;
        this.currentPlan = currentPlan;
        this.selection = [row];
        this.syncTableSelection('planTable', [row]);
        this.loadPlanDotLineDetail(currentPlan);
      },
      async loadPlanDotLineDetail(currentPlan) {
        this.dotLineLoading = true;
        this.planRoutingPayload = null;
        this.taskList = [];
        try {
          const planData = await getPlanDotLine({ planId: currentPlan.id });
          this.planRoutingPayload = planData || null;
          const details = planData?.detailList;
          if (details?.length) {
            this.taskList = [...details]
              .sort((a, b) => (a.taskSort ?? 0) - (b.taskSort ?? 0))
              .map((item, index) => this.normalizeDetailRow(item, index));
            // 班组排程的“执行对象”支持人员/工位，工位必须按当前工序查询；
            // 工厂排程仍然使用原来的班组执行对象，preload 内部会直接跳过。
            await this.preloadTaskWorkstationOptions();
            this.syncWorkOrderExecutionSelections();
            return;
          }

          if (!currentPlan.produceRoutingId) {
            this.$message.warning('暂无布点明细且缺少工艺路线，无法加载工序');
            return;
          }

          const list = await getTaskListById(currentPlan.produceRoutingId);
          this.taskList = (list || []).map((item, index) =>
            this.normalizeRoutingTaskRow(item, index)
          );
          // 首次没有布点明细时会从工艺路线生成工序，班组排程仍需补齐
          // 每道工序可派工位；工厂排程不加载这部分数据。
          await this.preloadTaskWorkstationOptions();
          this.syncWorkOrderExecutionSelections();
        } catch (e) {
          this.planRoutingPayload = null;
          this.taskList = [];
          this.$message.error(e.message || '获取工序布点信息失败');
        } finally {
          this.dotLineLoading = false;
        }
      },
      normalizeDetailRow(item, index) {
        const execType = toSafeNumber(item.executionType) ?? EXEC_TYPE.HOMEMADE;
        const outsource = execType === EXEC_TYPE.OUTSOURCE;
        const entrust = execType === EXEC_TYPE.ENTRUST;
        const homemade = execType === EXEC_TYPE.HOMEMADE;
        const orderMode = this.isOrderScheduling;
        const savedObjectType =
          orderMode && homemade
            ? item.executionObjectType ||
              item.executionTargetType ||
              item.executionAssigneeType ||
              (item.executionWorkstationId ? 'workstation' : 'person')
            : '';
        const objectId = orderMode
          ? item.executionObjectId ||
            item.executionAssigneeId ||
            item.executionUserId ||
            item.executionCrewId ||
            item.executionWorkstationId ||
            item.executionTeamId ||
            ''
          : item.executionTeamId ?? '';
        const objectName = orderMode
          ? item.executionObjectName ||
            item.executionAssigneeName ||
            item.executionUserName ||
            item.executionCrewName ||
            item.executionWorkstationName ||
            item.executionTeamName ||
            ''
          : item.executionTeamName ?? '';
        const dispatchTeamId =
          item.dispatchTeamId ||
          item.teamId ||
          item.firstTaskTeamId ||
          item.firstTeamId ||
          item.executionTeamId ||
          '';
        const dispatchTeamName =
          item.dispatchTeamName ||
          item.teamName ||
          item.firstTaskTeamName ||
          item.firstTeamName ||
          item.executionTeamName ||
          '';
        const dispatchTeamLeaderId =
          item.dispatchTeamLeaderId ||
          item.teamLeaderUserId ||
          item.leaderUserId ||
          item.executionTeamLeaderId ||
          '';
        const executionStartTime = formatDotLineDateTime(
          item.executionStartTime
        );
        const executionEndTime = formatDotLineDateTime(item.executionEndTime);
        return {
          ...item,
          _taskKey: item.id ?? `detail-${item.taskId ?? index}`,
          isParallelism: toSafeNumber(item.isParallelism) === 1 ? 1 : 0,
          executionStartTime,
          executionEndTime,
          dispatchDefaultStartTime: executionStartTime,
          dispatchDefaultEndTime: executionEndTime,
          executionType: execType,
          executionFactoryId: entrust
            ? item.executionFactoryId ?? item.executionTeamId ?? ''
            : '',
          executionFactoryName: entrust
            ? item.executionFactoryName ?? item.executionTeamName ?? ''
            : '',
          executionTeamId:
            homemade && orderMode && objectId
              ? `${savedObjectType}:${objectId}`
              : homemade
              ? item.executionTeamId ?? ''
              : '',
          rawExecutionTeamId: homemade && orderMode ? objectId : '',
          executionObjectType: homemade && orderMode ? savedObjectType : '',
          executionObjectName: homemade && orderMode ? objectName : '',
          dispatchTeamId,
          dispatchTeamName,
          dispatchTeamLeaderId,
          quantity: orderMode
            ? this.normalizeOrderQuantity(
                item.quantity ??
                  item.taskQuantity ??
                  item.requiredFormingNum ??
                  item.productNum ??
                  ''
              )
            : item.quantity,
          executionTeamLeader:
            outsource || entrust ? '' : item.executionTeamLeader ?? '',
          executionTeamLeaderId:
            outsource || entrust ? '' : item.executionTeamLeaderId ?? '',
          executionTeamName: outsource || entrust ? '' : objectName
        };
      },
      normalizeRoutingTaskRow(item, index) {
        const plan = this.currentPlan || {};
        const taskId = item.id;
        const row = {
          ...item,
          _taskKey: item.id ?? `new-${taskId ?? index}-${index}`,
          taskId: taskId ?? undefined,
          taskName: item.name ?? item.taskName,
          taskCode: item.code ?? item.taskCode,
          taskSort: item.taskSort ?? index + 1,
          type: item.type ?? undefined,
          sourceTaskId: item.sourceTaskId,
          planId: plan.id,
          planCode: plan.code,
          routingId: plan.produceRoutingId,
          routingName: plan.produceRoutingName ?? plan.routingName,
          isParallelism: 0,
          executionType: EXEC_TYPE.HOMEMADE,
          executionTeamId: '',
          executionObjectType: '',
          executionObjectName: '',
          dispatchTeamId:
            item.dispatchTeamId ||
            item.teamId ||
            item.firstTaskTeamId ||
            item.firstTeamId ||
            item.executionTeamId ||
            '',
          dispatchTeamName:
            item.dispatchTeamName ||
            item.teamName ||
            item.firstTaskTeamName ||
            item.firstTeamName ||
            item.executionTeamName ||
            '',
          dispatchTeamLeaderId:
            item.dispatchTeamLeaderId ||
            item.teamLeaderUserId ||
            item.leaderUserId ||
            item.executionTeamLeaderId ||
            '',
          executionTeamLeader: '',
          executionTeamLeaderId: '',
          executionTeamName: '',
          executionStartTime: '',
          executionEndTime: '',
          dispatchDefaultStartTime: '',
          dispatchDefaultEndTime: '',
          executionFactoryId: '',
          executionFactoryName: ''
        };
        if (this.isOrderScheduling) {
          row.quantity = '';
        }
        return row;
      },
      taskName(item, index) {
        return item.taskName || item.name || `工艺${index + 1}`;
      },
      routeStepTitle(item, index) {
        return (
          item.taskTypeName || item.taskName || item.name || `工艺${index + 1}`
        );
      },
      isTaskDone(item) {
        return item.executionType != null && item.executionType !== '';
      },
      execTypeNum(row) {
        return toSafeNumber(row.executionType);
      },
      clearExecutionTeamFields(row) {
        this.$set(row, 'executionTeamId', '');
        this.$set(row, 'executionTeamName', '');
        this.$set(row, 'executionObjectType', '');
        this.$set(row, 'executionObjectName', '');
        this.$set(row, 'executionTeamLeader', '');
        this.$set(row, 'executionTeamLeaderId', '');
        this.$set(row, 'executionFactoryId', '');
        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.findHomemadeOption(row);
          row.executionTeamName = team?.name ?? '';
          row.executionObjectType = team?.objectType ?? '';
          row.executionObjectName = team?.name ?? '';
          row.rawExecutionTeamId = team?.rawId ?? row.executionTeamId;
          row.executionTeamLeader = team?.leaderUserName ?? '';
          row.executionTeamLeaderId = team?.leaderUserId ?? '';
          row.executionFactoryId = '';
          row.executionFactoryName = '';
        } else if (t === EXEC_TYPE.ENTRUST) {
          const f = this.factoryList.find(
            (x) => x.value === row.executionFactoryId
          );
          row.executionFactoryName = f?.label ?? '';
          row.executionTeamId = '';
          row.executionTeamName = '';
          row.executionTeamLeader = '';
          row.executionTeamLeaderId = '';
        }
      },
      async loadTeamOptions() {
        try {
          const factoryId = this.$store.state.user.info.factoryId;
          const res = await teamPage({ pageNum: 1, size: -1, factoryId });
          this.teamOptions = (res?.list || []).map(
            ({ id, name, leaderUserName, leaderUserId }) => ({
              id,
              name,
              objectType: 'team',
              leaderUserName,
              leaderUserId
            })
          );
        } catch {
          this.teamOptions = [];
        }
      },
      async loadApprovalReviewConfig() {
        try {
          const res = await parameterGetByCode({
            code: 'planned_release_require'
          });
          this.isReview = res?.value == '1';
        } catch (e) {
          this.isReview = false;
        }
      },
      async loadWorkOrderExecutionOptions() {
        // 班组排程自制执行对象分两类：
        // - 人员：打开弹窗时一次性加载，所有工序共用。
        // - 工位：依赖具体工序，不能取全厂工位，需要在工序加载后按 taskId 查询。
        try {
          const res = await getUserPage({ pageNum: 1, size: -1 });
          const users = Array.isArray(res?.list) ? res.list : [];
          this.teamOptions = users.map((item) => ({
            id: `person:${item.id}`,
            rawId: item.id,
            objectType: 'person',
            name: `人员：${
              item.name ||
              item.userName ||
              item.loginName ||
              item.jobNumber ||
              item.id
            }`,
            leaderUserName: item.name || item.userName || '',
            leaderUserId: item.id
          }));
        } catch {
          this.teamOptions = [];
        }
        this.syncWorkOrderExecutionSelections();
      },
      getTaskWorkstationKey(row) {
        const rowTaskId = row?.taskId;
        const currentTaskId = this.currentPlan?.taskId;
        const taskId =
          row?.sourceTaskId ??
          row?.firstTaskId ??
          (rowTaskId && Number(rowTaskId) >= 0 ? rowTaskId : undefined) ??
          (currentTaskId && Number(currentTaskId) >= 0
            ? currentTaskId
            : undefined) ??
          this.currentPlan?.firstTaskId ??
          '';
        return taskId === null || taskId === undefined || taskId === ''
          ? ''
          : String(taskId);
      },
      formatWorkstationOptions(list = []) {
        return (Array.isArray(list) ? list : []).map((item) => {
          const substance =
            item.workstationSubstanceList?.[0]?.substance ||
            item.substance ||
            item.asset ||
            {};
          const category =
            substance.category?.category || substance.category || {};
          const name =
            item.name || item.workstationName || item.stationName || item.id;
          const code = item.code || item.workstationCode || item.stationCode;
          const displayName = code ? `${name}（${code}）` : name;
          const rawId =
            item.id ??
            item.workstationId ??
            item.factoryWorkstationId ??
            item.stationId ??
            code ??
            name;
          return {
            id: `workstation:${rawId}`,
            rawId,
            objectType: 'workstation',
            name: `工位：${displayName}`,
            code: item.code || item.workstationCode || item.stationCode || '',
            workstationCode:
              item.workstationCode || item.code || item.stationCode || '',
            assetCode: this.pickBackendTextValue(
              item.assetCode,
              item.deviceCode,
              item.extInfo?.assetCode,
              item.extInfo?.deviceCode,
              substance.code
            ),
            assetId:
              this.normalizeBackendLongId(item.assetId) ||
              this.normalizeBackendLongId(item.deviceId) ||
              this.normalizeBackendLongId(item.extInfo?.assetId) ||
              this.normalizeBackendLongId(substance.id) ||
              '',
            assetName: this.pickBackendTextValue(
              item.assetName,
              item.deviceName,
              item.extInfo?.assetName,
              item.extInfo?.deviceName,
              substance.name
            ),
            assetModelType:
              item.assetModelType ||
              item.deviceModel ||
              category.modelType ||
              '',
            assetCategoryLevelPath:
              item.assetCategoryLevelPath ||
              item.deviceType ||
              category.categoryLevelPath?.split('-')?.[0] ||
              '',
            extInfo: item.extInfo || {},
            isNew: item.isNew || '',
            leaderUserName: item.leaderName || '',
            leaderUserId: item.leaderId || ''
          };
        });
      },
      async loadWorkstationOptionsByTaskId(taskId) {
        const key =
          taskId === null || taskId === undefined || taskId === ''
            ? ''
            : String(taskId);
        // 这里复用任务派单“工序派单”的工位来源：
        // /main/factoryworkstation/listByFirstTaskId/{taskId}
        // 这样每行工序只展示它能派发的工位。isOrderScheduling 用来保证
        // 该接口不会在工厂排程中触发，避免改变现有工厂排程数据链路。
        if (
          !this.isOrderScheduling ||
          !key ||
          this.workstationOptionsMap[key] ||
          this.workstationOptionsLoadingMap[key]
        ) {
          return;
        }
        this.$set(this.workstationOptionsLoadingMap, key, true);
        try {
          const res = await listByFirstTaskId(taskId);
          this.$set(
            this.workstationOptionsMap,
            key,
            this.formatWorkstationOptions(res)
          );
          this.syncWorkOrderExecutionSelections();
        } catch {
          this.$set(this.workstationOptionsMap, key, []);
        } finally {
          this.$delete(this.workstationOptionsLoadingMap, key);
        }
      },
      preloadTaskWorkstationOptions(rows = this.taskList) {
        if (!this.isOrderScheduling) {
          return Promise.resolve();
        }
        // 多个工序可能引用同一个 taskId，去重后再请求，减少重复接口调用。
        const taskIds = Array.from(
          new Set(
            (rows || [])
              .map((row) => this.getTaskWorkstationKey(row))
              .filter(Boolean)
          )
        );
        return Promise.all(
          taskIds.map((taskId) => this.loadWorkstationOptionsByTaskId(taskId))
        );
      },
      getHomemadeOptions(row) {
        if (!this.isOrderScheduling) {
          return this.teamOptions;
        }
        // 班组排程的自制执行对象 = 人员 + 当前工序可派工位。
        // 下拉框每一行调用一次该方法，因此这里按行拿 taskId，再合并该行的
        // 工位缓存；若缓存还没回来，先显示人员，接口返回后 Vue 会刷新下拉。
        const key = this.getTaskWorkstationKey(row);
        if (key && !this.workstationOptionsMap[key]) {
          this.loadWorkstationOptionsByTaskId(key);
        }
        return [
          ...this.teamOptions,
          ...(key ? this.workstationOptionsMap[key] || [] : [])
        ];
      },
      findHomemadeOption(row) {
        return this.getHomemadeOptions(row).find((option) =>
          this.optionMatchesId(option.id, row.executionTeamId)
        );
      },
      syncWorkOrderExecutionSelections() {
        if (!this.isOrderScheduling) {
          return;
        }
        // 前端下拉为避免人员和工位 id 冲突，会加 person:/workstation: 前缀，
        // 所以工位选项加载完成后需要把原始 id 回填成下拉可识别的值。
        this.taskList.forEach((row) => {
          if (this.execTypeNum(row) !== EXEC_TYPE.HOMEMADE) return;
          const objectType = row.executionObjectType || 'person';
          const matched = this.getHomemadeOptions(row).find(
            (option) =>
              option.objectType === objectType &&
              this.optionMatchesId(option.rawId, row.rawExecutionTeamId)
          );
          if (matched) {
            this.$set(row, 'executionTeamId', matched.id);
            this.$set(row, 'executionTeamName', matched.name);
            this.$set(row, 'executionObjectName', matched.name);
            this.$set(row, 'rawExecutionTeamId', matched.rawId);
          }
        });
      },
      async loadFactoryOptions() {
        try {
          const res = await getFactoryarea({ type: 1, size: 9999 });
          if (res.list?.length) {
            this.factoryList = res.list.map((el) => ({
              value: el.id,
              label: el.name
            }));
          } else {
            this.factoryList = [];
          }
        } catch {
          this.factoryList = [];
        }
      },

      // Task config editing
      handleTimeChange(row, changeKey) {
        if (this.isCurrentPlanReadonly) {
          return;
        }
        if (isEndBeforeStart(row.executionStartTime, row.executionEndTime)) {
          this.$message.warning('执行结束时间不能小于执行开始时间');
          this.$set(row, changeKey, '');
          return;
        }
        if (
          changeKey === 'executionStartTime' &&
          this.isExecutionStartBeforePlanStart(row.executionStartTime)
        ) {
          this.$message.warning('执行开始时间不能小于计划开始时间');
          this.$set(row, changeKey, '');
          return;
        }
        if (!this.validateTaskSequence(row, { resetKey: changeKey })) {
          return;
        }
        const deadline =
          this.currentPlan?.planDeliveryTime || this.currentPlan?.deliveryTime;
        if (
          changeKey === 'executionStartTime' &&
          deadline &&
          row.executionStartTime
        ) {
          const d = new Date(deadline).getTime();
          const v = new Date(row.executionStartTime).getTime();
          if (!Number.isNaN(d) && !Number.isNaN(v) && v > d) {
            this.$message.warning('时间不能大于计划交付时间');
            this.$set(row, changeKey, '');
            return;
          }
        }
        if (changeKey === 'executionEndTime' && row.executionEndTime) {
          const planEnd = this.currentPlan?.endTime;
          const overDelivery =
            !!deadline && isEndAfterPlanEnd(row.executionEndTime, deadline);
          const overPlanEnd =
            !!planEnd && isEndAfterPlanEnd(row.executionEndTime, planEnd);
          if (overDelivery && overPlanEnd) {
            this.$message.warning(
              '执行结束时间晚于计划交付时间及计划结束时间，已保留填写内容，请留意核对'
            );
          } else if (overDelivery) {
            this.$message.warning(
              '执行结束时间晚于计划交付时间，已保留填写内容，请留意核对'
            );
          } else if (overPlanEnd) {
            this.$message.warning(
              '执行结束时间晚于计划结束时间，已保留填写内容，请留意核对'
            );
          }
        }
      },
      handleParallelismChange(row) {
        if (this.isCurrentPlanReadonly || !row?.executionStartTime) {
          return;
        }
        if (this.isTaskParallel(row)) {
          if (this.isExecutionStartBeforePlanStart(row.executionStartTime)) {
            this.$message.warning('执行开始时间不能小于计划开始时间');
            this.$set(row, 'executionStartTime', '');
          }
          return;
        }
        this.validateTaskSequence(row, { resetKey: 'executionStartTime' });
      },
      isExecutionEndOverDelivery(row) {
        const deadline =
          this.currentPlan?.planDeliveryTime || this.currentPlan?.deliveryTime;
        return (
          !!deadline &&
          !!row?.executionEndTime &&
          isEndAfterPlanEnd(row.executionEndTime, deadline)
        );
      },
      isExecutionEndOverPlanEnd(row) {
        const planEnd = this.currentPlan?.endTime;
        return (
          !!planEnd &&
          !!row?.executionEndTime &&
          isEndAfterPlanEnd(row.executionEndTime, planEnd)
        );
      },
      toggleDotLineFullscreen() {
        this.dotLineFullscreen = !this.dotLineFullscreen;
      },
      getTaskLabel(item, index) {
        if (item) {
          return (
            item.taskName ||
            item.name ||
            (typeof index === 'number' ? `工艺${index + 1}` : '当前工艺')
          );
        }
        return index !== undefined ? `工艺${index + 1}` : '当前工艺';
      },
      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();
        const endTime = new Date(end).getTime();
        return (
          !Number.isNaN(startTime) &&
          !Number.isNaN(endTime) &&
          startTime < endTime
        );
      },
      getCurrentPlanStartTime() {
        return (
          this.currentPlan?.startTime ||
          this.currentPlan?.planStartTime ||
          this.currentPlan?.executionStartTime ||
          ''
        );
      },
      isExecutionStartBeforePlanStart(executionStartTime) {
        if (this.isOrderScheduling || !executionStartTime) {
          return false;
        }
        const planStartTime = this.getCurrentPlanStartTime();
        if (!planStartTime) {
          return false;
        }
        const executionStart = new Date(executionStartTime).getTime();
        const planStart = new Date(planStartTime).getTime();
        return (
          !Number.isNaN(executionStart) &&
          !Number.isNaN(planStart) &&
          executionStart < planStart
        );
      },
      isTaskParallel(row) {
        return toSafeNumber(row?.isParallelism) === 1;
      },
      validateTaskSequence(row, options = {}) {
        const sequence = this.getTaskSequenceContext(row);
        if (!sequence) {
          return true;
        }
        const { index, prev, prevIndex, next, nextIndex } = sequence;
        const resetKey = options.resetKey;
        const checkPrev =
          (!resetKey || resetKey === 'executionStartTime') &&
          !this.isTaskParallel(row);
        const checkNext =
          (!resetKey || resetKey === 'executionEndTime') &&
          !this.isTaskParallel(next);

        if (
          checkPrev &&
          prev?.executionEndTime &&
          row.executionStartTime &&
          this.compareTaskTime(row.executionStartTime, prev.executionEndTime)
        ) {
          this.$message.warning(
            `${this.getTaskLabel(
              row,
              index
            )}执行开始时间不能小于上道工序${this.getTaskLabel(
              prev,
              prevIndex
            )}的执行结束时间`
          );
          if (resetKey) {
            this.$set(row, resetKey, '');
          }
          return false;
        }

        if (
          checkNext &&
          next?.executionStartTime &&
          row.executionEndTime &&
          this.compareTaskTime(next.executionStartTime, row.executionEndTime)
        ) {
          this.$message.warning(
            `下道工序${this.getTaskLabel(
              next,
              nextIndex
            )}的执行开始时间不能小于上道工序${this.getTaskLabel(
              row,
              index
            )}的执行结束时间`
          );
          if (resetKey) {
            this.$set(row, resetKey, '');
          }
          return false;
        }

        return true;
      },
      validateTaskRow(item, options = {}) {
        const label = (t) => this.getTaskLabel(t);
        const checkSequence = options.checkSequence !== false;
        const execType = toSafeNumber(item.executionType);
        if (execType === undefined) {
          this.$message.warning(`${label(item)}请选择执行模式`);
          return false;
        }

        if (execType === EXEC_TYPE.HOMEMADE) {
          if (
            !item.executionTeamId ||
            !item.executionStartTime ||
            !item.executionEndTime
          ) {
            this.$message.warning(
              `${label(
                item
              )}执行模式为自制时，需选择执行班组并填写执行开始和结束时间`
            );
            return false;
          }
        }

        if (execType === EXEC_TYPE.ENTRUST) {
          if (
            !item.executionFactoryId ||
            !item.executionStartTime ||
            !item.executionEndTime
          ) {
            this.$message.warning(
              `${label(
                item
              )}执行模式为请托时，需选择工厂并填写执行开始和结束时间`
            );
            return false;
          }
        }

        if (execType === EXEC_TYPE.OUTSOURCE) {
          if (!item.executionStartTime || !item.executionEndTime) {
            this.$message.warning(
              `${label(item)}执行模式为委外时，需填写执行开始和结束时间`
            );
            return false;
          }
        }

        if (isEndBeforeStart(item.executionStartTime, item.executionEndTime)) {
          this.$message.warning(
            `${label(item)}执行结束时间不能小于执行开始时间`
          );
          return false;
        }
        if (this.isExecutionStartBeforePlanStart(item.executionStartTime)) {
          this.$message.warning(
            `${label(item)}执行开始时间不能小于计划开始时间`
          );
          return false;
        }

        const deadline =
          this.currentPlan?.planDeliveryTime || this.currentPlan?.deliveryTime;
        if (deadline) {
          const d = new Date(deadline).getTime();
          const checkStart = (val, name) => {
            if (!val) return true;
            const t = new Date(val).getTime();
            return Number.isNaN(d) || Number.isNaN(t) || t <= d
              ? true
              : (this.$message.warning(
                  `${label(item)}${name}不能大于计划交付时间`
                ),
                false);
          };
          if (!checkStart(item.executionStartTime, '执行开始时间'))
            return false;
        }

        return checkSequence ? this.validateTaskSequence(item) : true;
      },
      validateTaskList() {
        for (const item of this.taskList) {
          if (!this.validateTaskRow(item)) return false;
        }
        return true;
      },
      addTaskRow(row) {
        if (row) {
          this.taskList.push(row);
        }
      },
      removeTaskRow(row) {
        const index = this.taskList.findIndex((item) => item === row);
        if (index >= 0) {
          this.taskList.splice(index, 1);
        }
      },
      addDispatchStations({ task, list }) {
        if (!this.isOrderScheduling || !task) {
          return;
        }
        const key = this.getTaskWorkstationKey(task);
        if (!key) {
          this.$message.warning('当前工序缺少任务信息，无法添加工位');
          return;
        }
        const currentList = this.workstationOptionsMap[key] || [];
        const nextList = [...currentList];
        const existedIds = new Set(currentList.map((item) => String(item.id)));
        this.formatWorkstationOptions(list).forEach((item) => {
          if (!existedIds.has(String(item.id))) {
            existedIds.add(String(item.id));
            nextList.push(item);
          }
        });
        this.$set(this.workstationOptionsMap, key, nextList);
      },
      removeDispatchStation({ task, row }) {
        if (!this.isOrderScheduling || !task || !row) {
          return;
        }
        const key = this.getTaskWorkstationKey(task);
        if (!key) {
          return;
        }
        const rowIds = new Set(
          [row.id, row.rawId, row.workstationId, row.factoryWorkstationId]
            .filter(
              (item) => item !== null && item !== undefined && item !== ''
            )
            .map((item) => String(item))
        );
        const currentList = this.workstationOptionsMap[key] || [];
        const nextList = currentList.filter((item) => {
          const itemIds = [
            item.id,
            item.rawId,
            item.workstationId,
            item.factoryWorkstationId
          ]
            .filter(
              (value) => value !== null && value !== undefined && value !== ''
            )
            .map((value) => String(value));
          return !itemIds.some((id) => rowIds.has(id));
        });
        this.$set(this.workstationOptionsMap, key, nextList);
      },
      async handleDispatchActionSuccess(event = {}) {
        if (!this.isOrderScheduling) {
          return;
        }
        if (event.type === 'confirm') {
          await this.reloadOrderSchedulingOrdersByDateRange(this.dateRange);
          return;
        }
        await this.refreshSchedulingView();
      },
      buildSavePayload(isCache = 0) {
        const plan = this.currentPlan;
        const head = this.planRoutingPayload || {};

        const detailList = this.taskList.map((row) => {
          const execType = toSafeNumber(row.executionType) ?? 0;
          // 只有班组排程需要把带前缀的下拉值还原为人员/工位原始 id；
          // 工厂排程保持原 executionTeamId 保存方式。
          const homemadeOption = this.isOrderScheduling
            ? this.findHomemadeOption(row)
            : null;
          const detail = {
            executionEndTime: row.executionEndTime || undefined,
            executionStartTime: row.executionStartTime || undefined,
            executionTeamId:
              execType === EXEC_TYPE.HOMEMADE
                ? (this.isOrderScheduling
                    ? homemadeOption?.rawId ||
                      row.rawExecutionTeamId ||
                      row.executionTeamId
                    : row.executionTeamId) || undefined
                : undefined,
            executionTeamLeader: row.executionTeamLeader || '',
            executionTeamLeaderId: row.executionTeamLeaderId || '',
            executionTeamName: row.executionTeamName || '',
            executionType: execType,
            isParallelism: this.isTaskParallel(row) ? 1 : 0,
            planCode: row.planCode || plan?.code,
            planId: row.planId ?? plan?.id,
            routingId:
              row.routingId ?? plan?.produceRoutingId ?? head.routingId,
            routingName:
              row.routingName || plan?.produceRoutingName || head.routingName,
            sourceTaskId: row.sourceTaskId ?? row.taskId,
            taskCode: row.taskCode,
            taskId: row.taskId,
            taskName: row.taskName,
            taskSort: row.taskSort,
            type: toSafeNumber(row.type)
          };
          // 数量输入框是班组排程工序配置新增字段，工厂排程不写入，
          // 避免改变原工厂排程保存 payload。
          if (this.isOrderScheduling) {
            detail.quantity =
              row.quantity !== '' &&
              row.quantity !== null &&
              row.quantity !== undefined
                ? row.quantity
                : undefined;
          }
          // 班组排程自制模式下，执行对象可能是人员也可能是工位；
          // executionObject* 字段用于明确记录对象类型和值，委外/外协不写入。
          if (this.isOrderScheduling && execType === EXEC_TYPE.HOMEMADE) {
            detail.executionObjectId =
              homemadeOption?.rawId ||
              row.rawExecutionTeamId ||
              row.executionTeamId ||
              undefined;
            detail.executionObjectType = row.executionObjectType || undefined;
            detail.executionObjectName =
              row.executionObjectName || row.executionTeamName || '';
          }
          if (execType === EXEC_TYPE.ENTRUST) {
            detail.executionFactoryId = row.executionFactoryId || undefined;
            detail.executionFactoryName = row.executionFactoryName || '';
          }
          if (row.id != null) detail.id = row.id;
          return detail;
        });

        return {
          detailList,
          fileParam: head.fileParam ?? [],
          id: head.id,
          isCache,
          planCode: plan?.code ?? head.planCode,
          planId: plan?.id ?? head.planId,
          produceVersionId: head.produceVersionId,
          produceVersionName: head.produceVersionName,
          releaseTime: head.releaseTime,
          routingCode: head.routingCode,
          routingId: head.routingId ?? plan?.produceRoutingId,
          routingName: head.routingName ?? plan?.produceRoutingName,
          routingVersion: head.routingVersion
        };
      },

      // Scheduling data refresh
      async handleTabChange(tabKey) {
        this.activeTab = tabKey;
        await this.refreshSchedulingView();
        if (!this.isOrderScheduling) {
          await this.reloadPlanSchedulingPlansByStatus();
        }
      },

      async handleDateChange(val) {
        this.dateRange = val;
        if (this.isOrderScheduling) {
          this.scheduleOrderSchedulingDateReload(val);
          return;
        }
        await this.refreshSchedulingView();
        await this.reloadPlanSchedulingPlansByStatus();
      },

      handleDateInput(val) {
        if (!this.isOrderScheduling) {
          return;
        }
        this.dateRange = val;
        this.scheduleOrderSchedulingDateReload(val);
      },

      scheduleOrderSchedulingDateReload(dateRange) {
        if (!Array.isArray(dateRange) || dateRange.length !== 2) {
          return;
        }
        if (!dateRange[0] || !dateRange[1]) {
          return;
        }
        if (this.orderDateReloadTimer) {
          clearTimeout(this.orderDateReloadTimer);
        }
        this.orderDateReloadTimer = setTimeout(() => {
          this.orderDateReloadTimer = null;
          this.reloadOrderSchedulingOrdersByDateRange(dateRange);
        }, 0);
      },

      async reloadOrderSchedulingOrdersByDateRange(dateRange) {
        this.orderDetailReadonly = false;
        this.orderDetailActionMode = '';
        if (typeof this.orderListLoader !== 'function') {
          await this.refreshSchedulingView();
          return;
        }
        const loading = this.$loading({
          lock: true,
          text: '加载订单中...'
        });
        try {
          const list = await this.orderListLoader(
            dateRange,
            this.getPlanTableStatusRequestWhere()
          );
          this.selectedWorkOrders = this.buildWorkOrderTableRows(list);
          this.planDataList = this.selectedWorkOrders;
          const currentWorkOrderId = this.currentPlan?.workOrderId;
          const matchedCurrentOrder = currentWorkOrderId
            ? this.selectedWorkOrders.find(
                (item) => String(item.id) === String(currentWorkOrderId)
              )
            : null;
          if (matchedCurrentOrder) {
            const currentPlan = this.resolveCurrentPlan(matchedCurrentOrder);
            this.orderDetailReadonly =
              this.isOrderSchedulingReadonlyRow(matchedCurrentOrder);
            this.orderDetailActionMode = this.orderDetailReadonly
              ? 'view'
              : 'edit';
            currentPlan._orderDetailReadonly = this.orderDetailReadonly;
            currentPlan.orderDetailReadonly = this.orderDetailReadonly;
            this.currentPlan = currentPlan;
            this.selection = [matchedCurrentOrder];
            this.syncTableSelection('planTable', [matchedCurrentOrder]);
            await this.loadPlanDotLineDetail(currentPlan);
          } else {
            this.orderDetailReadonly = false;
            this.orderDetailActionMode = '';
            this.currentPlan = null;
            this.planRoutingPayload = null;
            this.taskList = [];
            this.selection = [];
            this.syncTableSelection('planTable', []);
          }

          await this.refreshSchedulingView();
        } catch (e) {
          this.$message.error(e.message || '获取订单数据失败');
        } finally {
          loading.close();
        }
      },
      extractPlanIdsFromGanttData(data = []) {
        const planIds = new Set();
        (Array.isArray(data) ? data : []).forEach((item) => {
          (item.resourcePeriods || []).forEach((period) => {
            if (period.planId) planIds.add(period.planId);
          });
          (item.resourceList || []).forEach((resource) => {
            if (resource.planId) planIds.add(resource.planId);
          });
        });
        return Array.from(planIds);
      },
      async syncPlanTableRowsByGanttData(data = []) {
        if (this.isOrderScheduling) {
          return;
        }
        const planIds = this.extractPlanIdsFromGanttData(data);
        if (!planIds.length) {
          return;
        }
        const plans = await this.loadPlansByIds(planIds);
        this.planDataList = this.buildPlanTableRows(plans);
      },

      async refreshSchedulingView() {
        let data;
        const params = {};
        if (this.dateRange && this.dateRange.length === 2) {
          params.startDate = this.dateRange[0];
          params.endDate = this.dateRange[1];
        }
        // 班组排程从工单页面进入，用户选择的是工单数据；
        // 甘特接口仍按计划维度查询，所以这里提取工单关联的 productionPlanId
        // 作为 planIds 过滤条件。工厂排程本身就是计划入口，不需要该过滤。
        if (this.isOrderScheduling) {
          const planIds = Array.from(
            new Set(
              (this.selectedWorkOrders || [])
                .map((item) => item.productionPlanId)
                .filter((id) => id !== null && id !== undefined && id !== '')
            )
          );
          if (planIds.length) {
            params.planIds = planIds;
          } else {
            this.calendarSourceData = [];
            this.tableData = [];
            return;
          }
        }
        if (this.activeTab === 'factory') {
          // 工厂维度甘特：
          // - 工厂排程默认进入该页签，继续使用 factorySchedulingGanttChart。
          data = await factorySchedulingGanttChart(params);

          await this.syncPlanTableRowsByGanttData(data);
        } else if (this.isOrderScheduling) {
          // 班组排程甘特按工单关联计划过滤，再使用工位/设备/人员专用接口。
          // 展示结构仍交给现有甘特组件处理，保持和工厂排产一致的树形时间轴形式。
          data = await workstationSchedulingGanttChart(params);
          data = this.filterOrderSchedulingGanttDataByTab(data || []);
        } else {
          // 班组/工位维度甘特：
          // 工厂排程切到该页签时保持原班组甘特逻辑；
          data = await teamSchedulingGanttChart(params);

          await this.syncPlanTableRowsByGanttData(data);
        }
        this.calendarSourceData = this.normalizeOrderSchedulingGanttData(
          data || []
        );
        this.tableData = this.initGanttFlatRows(this.calendarSourceData);
      },
      getOrderSchedulingGanttWorkstationType(tabKey = this.activeTab) {
        const workstationTypeMap = {
          line: 1,
          person: 3
        };
        return workstationTypeMap[tabKey];
      },
      hasOrderSchedulingDeviceResource(item = {}) {
        return !!(item.deviceId || item.deviceCode || item.deviceName);
      },
      getOrderSchedulingAssigneeUsers(item = {}) {
        if (Array.isArray(item.assigneeUser)) {
          return item.assigneeUser;
        }
        if (item.assigneeUser && typeof item.assigneeUser === 'object') {
          return [item.assigneeUser];
        }
        return [];
      },
      buildOrderSchedulingDeviceResource(item = {}) {
        const deviceName =
          item.deviceName || item.equipmentName || item.assetName || '';
        const deviceCode = item.deviceCode || item.assetCode || '';
        const deviceId =
          this.normalizeBackendLongId(item.deviceId) ||
          this.normalizeBackendLongId(item.assetId) ||
          this.normalizeBackendLongId(item.extInfo?.assetId);
        const resourceId =
          deviceId || deviceCode || deviceName || item.resourceId;
        const workstationName = item.name || item.resourceName || '';
        const workstationCode = item.workstationCode || item.code || '';
        return {
          ...item,
          resourceId: `device:${resourceId || ''}`,
          resourceName: deviceName || deviceCode || workstationName,
          resourceCode: deviceCode,
          deviceId,
          deviceCode,
          deviceName,
          equipmentName: deviceName || deviceCode,
          workstationName,
          workstationCode,
          originResourceName: item.resourceName || item.name || '',
          originResourceCode: item.resourceCode || item.code || '',
          orderResourceTabType: 'device'
        };
      },
      buildOrderSchedulingPersonResources(item = {}) {
        const workstationName = item.name || item.resourceName || '';
        const workstationCode = item.workstationCode || item.code || '';
        return this.getOrderSchedulingAssigneeUsers(item)
          .map((user, index) => {
            const userName =
              user?.userName || user?.name || user?.nickName || '';
            const userId = user?.userId || user?.id || userName;
            const jobNumber = user?.jobNumber || user?.code || '';
            if (!userName && !userId && !jobNumber) {
              return null;
            }
            return {
              ...item,
              resourceId: `person:${userId || index}-${
                item.resourceId || item.taskId || item.sourceTaskId || index
              }`,
              resourceName: userName || jobNumber || String(userId),
              resourceCode: jobNumber || String(userId || ''),
              assigneeUser: [user],
              assigneeUserId: userId,
              assigneeUserName: userName,
              assigneeJobNumber: jobNumber,
              executionObjectType: 'person',
              executionObjectName: userName || jobNumber || String(userId),
              workstationName,
              workstationCode,
              originResourceName: item.resourceName || item.name || '',
              originResourceCode: item.resourceCode || item.code || '',
              orderResourceTabType: 'person'
            };
          })
          .filter(Boolean);
      },
      filterOrderSchedulingGanttDataByTab(data = []) {
        const list = Array.isArray(data) ? data : [];
        if (this.activeTab === 'line') {
          return list
            .filter((item) => this.hasOrderSchedulingDeviceResource(item))
            .map((item) => this.buildOrderSchedulingDeviceResource(item));
        }
        if (this.activeTab === 'person') {
          return list.flatMap((item) =>
            this.buildOrderSchedulingPersonResources(item)
          );
        }
        const targetWorkstationType =
          this.getOrderSchedulingGanttWorkstationType();
        if (!targetWorkstationType) {
          return list;
        }
        return list.filter(
          (item) => Number(item?.workstationType) === targetWorkstationType
        );
      },
      getOrderSchedulingResourceInfo(item = {}) {
        const resourceName = item.resourceName || item.name || '';
        const resourceCode = item.resourceCode || item.code || '';
        const resourceType = Number(item.type);
        const workstationType = Number(item.workstationType);
        const deviceName =
          item.deviceName || item.equipmentName || item.assetName || '';
        const deviceCode = item.deviceCode || item.assetCode || '';
        const personName =
          item.executionObjectName || item.assigneeUserName || '';
        return {
          resourceName,
          resourceCode,
          resourceType,
          workstationType,
          orderResourceTabType: item.orderResourceTabType || '',
          deviceId:
            this.normalizeBackendLongId(item.deviceId) ||
            this.normalizeBackendLongId(item.assetId) ||
            this.normalizeBackendLongId(item.extInfo?.assetId) ||
            '',
          deviceCode,
          deviceName,
          assigneeUser: item.assigneeUser || [],
          assigneeUserId: item.assigneeUserId || '',
          assigneeUserName: item.assigneeUserName || '',
          assigneeJobNumber: item.assigneeJobNumber || '',
          workstationName:
            item.workstationName ||
            item.stationName ||
            (resourceType === 1 ? resourceName : ''),
          equipmentName:
            item.equipmentName ||
            deviceName ||
            item.assetName ||
            (item.orderResourceTabType === 'device' ? resourceName : ''),
          executionObjectType:
            item.executionObjectType || (workstationType === 3 ? 'person' : ''),
          executionObjectName:
            item.executionObjectName ||
            personName ||
            (workstationType === 3 ? resourceName : '')
        };
      },
      normalizeOrderSchedulingPlanRef(source = {}) {
        const productionOrderNumber =
          source.productionOrderNumber ||
          source.apsWorkOrderCode ||
          source.workOrderCode ||
          source.orderCode ||
          '';
        return {
          planId: source.planId ?? source.apsWorkOrderId ?? source.workOrderId,
          planCode:
            source.planCode ||
            source.apsWorkOrderCode ||
            source.workOrderCode ||
            '',
          productionOrderNumber
        };
      },
      getOrderSchedulingDisplayOrderCode(...sources) {
        for (const source of sources) {
          if (!source) continue;
          const code =
            source.productionOrderNumber ||
            source.apsWorkOrderCode ||
            source.workOrderCode ||
            source.orderCode ||
            source.planCode ||
            '';
          if (code) return code;
        }
        return '';
      },
      buildGanttPlanLabel(planInfo = {}, fallbackInfo = {}) {
        const displayCode = this.isOrderScheduling
          ? this.getOrderSchedulingDisplayOrderCode(planInfo, fallbackInfo)
          : planInfo.planCode || '';
        return [displayCode, planInfo.productName || fallbackInfo.productName]
          .filter(Boolean)
          .join(' ');
      },
      normalizeOrderSchedulingGanttData(data = []) {
        if (!this.isOrderScheduling) {
          return data;
        }
        return (Array.isArray(data) ? data : []).map((item) => {
          const resourceInfo = this.getOrderSchedulingResourceInfo(item);
          const resourceList = Array.isArray(item.resourceList)
            ? item.resourceList.map((resource) => {
                const planRef = this.normalizeOrderSchedulingPlanRef(resource);
                return {
                  ...resource,
                  ...planRef,
                  productName: resource.productName || '',
                  workstationName:
                    resource.workstationName ||
                    resource.stationName ||
                    (resourceInfo.resourceType === 1
                      ? resourceInfo.resourceName
                      : ''),
                  equipmentName:
                    resource.equipmentName ||
                    resource.deviceName ||
                    resource.assetName ||
                    (resourceInfo.orderResourceTabType === 'device'
                      ? resourceInfo.resourceName
                      : ''),
                  deviceId:
                    this.normalizeBackendLongId(resource.deviceId) ||
                    this.normalizeBackendLongId(resource.assetId) ||
                    this.normalizeBackendLongId(resource.extInfo?.assetId) ||
                    resourceInfo.deviceId ||
                    '',
                  deviceCode:
                    resource.deviceCode || resourceInfo.deviceCode || '',
                  deviceName:
                    resource.deviceName || resourceInfo.deviceName || '',
                  assigneeUser:
                    resource.assigneeUser || resourceInfo.assigneeUser || [],
                  assigneeUserId:
                    resource.assigneeUserId ||
                    resourceInfo.assigneeUserId ||
                    '',
                  assigneeUserName:
                    resource.assigneeUserName ||
                    resourceInfo.assigneeUserName ||
                    '',
                  assigneeJobNumber:
                    resource.assigneeJobNumber ||
                    resourceInfo.assigneeJobNumber ||
                    '',
                  executionObjectName:
                    resource.executionObjectName ||
                    (resourceInfo.orderResourceTabType === 'person'
                      ? resourceInfo.resourceName
                      : '')
                };
              })
            : [];
          const resourcePeriods = Array.isArray(item.resourcePeriods)
            ? item.resourcePeriods.map((period) => ({
                ...period,
                ...this.normalizeOrderSchedulingPlanRef(period)
              }))
            : [];
          return {
            ...item,
            resourceCode: resourceInfo.resourceCode,
            resourceName: resourceInfo.resourceName,
            resourceList,
            resourcePeriods,
            orderResourceTabType: resourceInfo.orderResourceTabType,
            deviceId: resourceInfo.deviceId,
            deviceCode: resourceInfo.deviceCode,
            deviceName: resourceInfo.deviceName,
            assigneeUser: resourceInfo.assigneeUser,
            assigneeUserId: resourceInfo.assigneeUserId,
            assigneeUserName: resourceInfo.assigneeUserName,
            assigneeJobNumber: resourceInfo.assigneeJobNumber,
            workstationName: resourceInfo.workstationName,
            equipmentName: resourceInfo.equipmentName,
            executionObjectType: resourceInfo.executionObjectType,
            executionObjectName: resourceInfo.executionObjectName
          };
        });
      },
      buildGanttPeriodTaskId(parentId, matchedPlanInfo, period, periodIndex) {
        const periodTaskId = [
          matchedPlanInfo.planId || period.planId || matchedPlanInfo.planCode,
          period.apsWorkOrderId || period.workOrderId || period.planCode,
          period.startTime,
          period.endTime,
          periodIndex
        ]
          .filter(
            (value) => value !== '' && value !== null && value !== undefined
          )
          .join('-');
        return `${parentId}-${periodTaskId || periodIndex}`;
      },
      async open(row) {
        const ids = Array.isArray(row?.ids) ? row.ids : [];
        const readonly = !!row?.readonly;
        const mode = row?.mode === 'order' ? 'order' : 'plan';
        const selectedWorkOrders = Array.isArray(row?.selectedWorkOrders)
          ? row.selectedWorkOrders
          : [];
        const orderListLoader =
          typeof row?.orderListLoader === 'function'
            ? row.orderListLoader
            : null;
        const loading = ids.length
          ? this.$loading({
              lock: true,
              text: mode === 'order' ? '加载订单中...' : '加载计划中...'
            })
          : 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.schedulingMode = mode;
        this.planTableStatusTab = this.getDefaultPlanTableStatusTab(mode);
        this.orderListLoader = orderListLoader;
        // 弹窗复用同一个组件，但入口数据不同：
        // - plan：生产计划行，使用工厂排程列 createSchedulingColumns。
        // - order：工单行，使用班组排程列 createWorkOrderSchedulingColumns。
        this.columns =
          mode === 'order'
            ? createWorkOrderSchedulingColumns()
            : createSchedulingColumns();
        this.selectedWorkOrders =
          this.buildWorkOrderTableRows(selectedWorkOrders);
        if (mode === 'order' && this.selectedWorkOrders.length) {
          // 班组排程表格展示工单，但工序布点和甘特仍依赖生产计划 id；
          // 因此把工单里的 productionPlan* 字段映射成 currentPlan 可识别的形态。
          selectedPlans = this.selectedWorkOrders
            .filter((item) => item.productionPlanId)
            .map((item) => ({
              ...item,
              id: item.productionPlanId,
              code: item.productionPlanCode,
              startTime: item.planStartTime,
              endTime: item.planCompleteTime,
              productNum: item.formingNum,
              unit: item.measuringUnit
            }));
          allPlans = selectedPlans;
        }
        this.entryReadonly = readonly;
        this.orderDetailReadonly = false;
        this.orderDetailActionMode = '';
        this.allPlanDataList = deepClone(allPlans || []);
        this.planDataList =
          mode === 'order'
            ? this.selectedWorkOrders
            : this.buildPlanTableRows(selectedPlans, this.allPlanDataList);
        this.currentPlan = null;
        this.planRoutingPayload = null;
        this.taskList = [];
        // 每次打开弹窗都清理按工序缓存，避免上一次班组排程的工位
        // 泄露到新的工单/计划中。
        this.workstationOptionsMap = {};
        this.workstationOptionsLoadingMap = {};
        this.selection = [];
        this.planPickerVisible = false;
        this.planPickerSelection = [];
        this.dotLineLoading = false;
        this.dotLineFullscreen = false;
        this.calendarSourceData = [];
        this.tableData = [];
        this.activeTab = mode === 'order' ? 'team' : 'factory';
        const now = new Date();
        this.dateRange = [
          `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(
            2,
            '0'
          )}-01`,
          `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(
            2,
            '0'
          )}-${String(
            new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()
          ).padStart(2, '0')}`
        ];
        if (mode === 'order') {
          // 班组排程只预加载人员选项；工位必须等工序列表出来后，
          // 按每道工序 taskId 调任务派单同款接口补齐。
          await this.loadWorkOrderExecutionOptions();
        } else {
          // 工厂排程仍使用班组主数据作为自制执行对象，不走人员/工位混合逻辑。
          await this.loadTeamOptions();
        }
        if (mode === 'order') {
          await this.reloadPlanTableByStatus();
        } else {
          await this.refreshSchedulingView();
          await this.reloadPlanSchedulingPlansByStatus();
        }

        this.visible = true;
      },

      // Legacy operations kept for compatibility
      swap(index1, index2) {
        let temp1 = deepClone(this.form.scheduledPlans[index1]);
        let temp2 = deepClone(this.form.scheduledPlans[index2]);
        this.$set(this.form.scheduledPlans, index1, temp2);
        this.$set(this.form.scheduledPlans, index2, temp1);
        this.indexSort();
      },
      indexSort() {
        this.form.scheduledPlans.forEach((item, index) => {
          this.$set(this.form.scheduledPlans[index], 'serialNumber', index + 1);
        });
      },
      remove() {
        let codeS = this.selection.map((item) => item.code);
        this.form.scheduledPlans = this.form.scheduledPlans.filter(
          (item) => !codeS.includes(item.code)
        );
        this.indexSort();
      },
      moveUp() {
        let index = this.selection[0].serialNumber - 1;
        if (index > 0) {
          this.swap(index, index - 1);
        }
      },

      moveDown() {
        let index = this.selection[0].serialNumber - 1;
        if (index < this.form.scheduledPlans.length - 1) {
          this.swap(index, index + 1);
        }
      },
      exportList() {
        const plans = this.form.scheduledPlans || [];
        if (!plans.length) {
          this.$message.warning('暂无可导出数据');
          return;
        }
        const headers = [
          '序号',
          '计划状态',
          '计划编号',
          '生产订单号',
          '产线名称',
          '计划开始时间',
          '计划结束时间',
          '交期',
          '计划数量',
          '单位',
          '优先级'
        ];
        const escapeCsv = (value) => {
          const text =
            value === null || value === undefined ? '' : String(value);
          return `"${text.replace(/"/g, '""')}"`;
        };
        const rows = plans.map((item, index) =>
          [
            item.serialNumber || index + 1,
            item.mesStatusName,
            item.code,
            item.productionOrderNumber,
            item.productionLineName,
            item.startTime,
            item.endTime,
            item.deliveryTime,
            item.productNum,
            item.unit,
            item.priority
          ]
            .map((cell) => escapeCsv(cell))
            .join(',')
        );
        const csvContent = [headers.join(','), ...rows].join('\n');
        const blob = new Blob([`\uFEFF${csvContent}`], {
          type: 'text/csv;charset=utf-8;'
        });
        const url = window.URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = `factory_production_scheduling_${Date.now()}.csv`;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        window.URL.revokeObjectURL(url);
      },
      initGanttFlatRows(data) {
        if (!Array.isArray(data) || data.length === 0) {
          return [];
        }
        const rows = [];
        data.forEach((item, index) => {
          const planList = Array.isArray(item.resourceList)
            ? item.resourceList
            : [];
          const planInfo = planList[0] || {};
          const taskLabel = item.taskName || item.name || '工序';
          const planLabel = this.buildGanttPlanLabel(planInfo);
          const resourceLabel =
            planInfo.teamName ||
            planInfo.productionLineName ||
            item.name ||
            item.taskName ||
            item.resourceId ||
            `资源${index + 1}`;
          const teamName = planInfo.teamName || resourceLabel;
          const productionLineName =
            planInfo.productionLineName || resourceLabel;
          const resourceName = this.isOrderScheduling
            ? item.resourceName || item.name || ''
            : '';
          const resourceCode = this.isOrderScheduling
            ? item.resourceCode || item.code || ''
            : '';
          const periods = Array.isArray(item.resourcePeriods)
            ? item.resourcePeriods
            : [];
          const parentId = `gantt-${item.resourceId || 'resource'}-${
            item.taskId || item.sourceTaskId || index
          }`;

          rows.push({
            id: parentId,
            code:
              item.taskId || item.sourceTaskId || planInfo.planCode || parentId,
            type: 2,
            name: taskLabel,
            text: taskLabel,
            executor: planLabel || taskLabel,
            laneLabel: resourceLabel,
            resourceName,
            resourceCode,
            teamName,
            productionOrderNumber: planInfo.productionOrderNumber || '',
            productionLineName,
            workstationName:
              item.workstationName ||
              item.stationName ||
              planInfo.workstationName ||
              planInfo.stationName ||
              resourceLabel,
            equipmentName:
              item.equipmentName ||
              item.deviceName ||
              item.assetName ||
              planInfo.equipmentName ||
              planInfo.deviceName ||
              planInfo.assetName ||
              '',
            deviceId:
              this.normalizeBackendLongId(item.deviceId) ||
              this.normalizeBackendLongId(item.assetId) ||
              this.normalizeBackendLongId(planInfo.deviceId) ||
              this.normalizeBackendLongId(planInfo.assetId) ||
              '',
            deviceCode: item.deviceCode || planInfo.deviceCode || '',
            deviceName: item.deviceName || planInfo.deviceName || '',
            assetName: item.assetName || planInfo.assetName || '',
            assigneeUser: item.assigneeUser || planInfo.assigneeUser || [],
            assigneeUserId:
              item.assigneeUserId || planInfo.assigneeUserId || '',
            assigneeUserName:
              item.assigneeUserName || planInfo.assigneeUserName || '',
            assigneeJobNumber:
              item.assigneeJobNumber || planInfo.assigneeJobNumber || '',
            executionObjectType: item.executionObjectType || '',
            executionObjectName: item.executionObjectName || '',
            render: periods.length ? 'split' : '',
            open: true
          });

          periods.forEach((period, periodIndex) => {
            const matchedPlanInfo =
              planList.find(
                (planItem) =>
                  String(planItem.planId || '') === String(period.planId || '')
              ) ||
              planList[periodIndex] ||
              planInfo;
            const childPlanLabel = this.buildGanttPlanLabel(
              matchedPlanInfo,
              period
            );
            const childDisplayOrderCode = this.isOrderScheduling
              ? this.getOrderSchedulingDisplayOrderCode(matchedPlanInfo, period)
              : matchedPlanInfo.planCode;
            rows.push({
              id: this.buildGanttPeriodTaskId(
                parentId,
                matchedPlanInfo,
                period,
                periodIndex
              ),
              parent: parentId,
              code:
                matchedPlanInfo.planCode ||
                item.taskId ||
                item.sourceTaskId ||
                `${parentId}-${periodIndex}`,
              planId: matchedPlanInfo.planId || period.planId || '',
              planCode: matchedPlanInfo.planCode || '',
              productionOrderNumber:
                matchedPlanInfo.productionOrderNumber ||
                period.productionOrderNumber ||
                period.apsWorkOrderCode ||
                period.workOrderCode ||
                '',
              productName: matchedPlanInfo.productName || '',
              productCode: matchedPlanInfo.productCode || '',
              routingName:
                matchedPlanInfo.routingName ||
                matchedPlanInfo.produceRoutingName ||
                '',
              batchNo: matchedPlanInfo.batchNo || '',
              specification: matchedPlanInfo.specification || '',
              modeType: matchedPlanInfo.modeType || '',
              workstationName:
                matchedPlanInfo.workstationName ||
                matchedPlanInfo.stationName ||
                item.workstationName ||
                item.stationName ||
                '',
              equipmentName:
                matchedPlanInfo.equipmentName ||
                matchedPlanInfo.deviceName ||
                matchedPlanInfo.assetName ||
                item.equipmentName ||
                item.deviceName ||
                item.assetName ||
                '',
              deviceId:
                this.normalizeBackendLongId(matchedPlanInfo.deviceId) ||
                this.normalizeBackendLongId(matchedPlanInfo.assetId) ||
                this.normalizeBackendLongId(item.deviceId) ||
                this.normalizeBackendLongId(item.assetId) ||
                '',
              deviceCode: matchedPlanInfo.deviceCode || item.deviceCode || '',
              deviceName: matchedPlanInfo.deviceName || item.deviceName || '',
              assetName: matchedPlanInfo.assetName || item.assetName || '',
              assigneeUser:
                matchedPlanInfo.assigneeUser || item.assigneeUser || [],
              assigneeUserId:
                matchedPlanInfo.assigneeUserId || item.assigneeUserId || '',
              assigneeUserName:
                matchedPlanInfo.assigneeUserName || item.assigneeUserName || '',
              assigneeJobNumber:
                matchedPlanInfo.assigneeJobNumber ||
                item.assigneeJobNumber ||
                '',
              executionObjectType: item.executionObjectType || '',
              executionObjectName: item.executionObjectName || '',
              type: 5,
              name:
                [taskLabel, childPlanLabel].filter(Boolean).join(' / ') ||
                taskLabel,
              text: [
                taskLabel,
                childDisplayOrderCode,
                matchedPlanInfo.productName,
                matchedPlanInfo.batchNo
              ]
                .filter(Boolean)
                .join(' / '),
              executor: childPlanLabel || taskLabel,
              laneLabel: resourceLabel,
              resourceName,
              resourceCode,
              teamName,
              productionLineName,
              start_date: formatDotLineDateTime(period.startTime),
              end_date: formatDotLineDateTime(period.endTime)
            });
          });
        });
        return rows;
      },
      // 添加班组行合并
      // addDuplicateCount(arr) {
      //   const elementCount = {}; // 用于存储元素及其出现次数
      //   arr.forEach((element, index) => {
      //     if (elementCount[element.shiftGroupCode] === undefined) {
      //       // 第一次出现，记录索引
      //       elementCount[element.shiftGroupCode] = { index, count: 1 };
      //     } else {
      //       // 不是第一次出现，增加计数
      //       elementCount[element.shiftGroupCode].count++;
      //     }
      //   });
      //   arr.forEach((item, index) => {
      //     if (elementCount[item.shiftGroupCode]?.index == index) {
      //       item.groupRowSpan = elementCount[item.shiftGroupCode].count;
      //     }
      //   });
      //   return arr;
      // },

      async handleApproveCurrentPlan() {
        const row = this.currentPlan;
        if (!row?.id) {
          this.$message.warning('请先勾选一条计划后再审批');
          return;
        }
        if (!row.produceRoutingName && !row.routingName) {
          this.$message.error('请先选择工艺路线！');
          return;
        }
        if (String(row.dotLineStatus) !== '1') {
          this.$message.error('请先完成布点！');
          return;
        }
        if (!this.canApproveCurrentPlan) {
          this.$message.warning('当前计划不满足审批条件');
          return;
        }

        const titleText = this.isReview
          ? '是否发起流程'
          : '审批工单后不可撤回，确定审批吗？';
        const title = this.isReview ? '流程确认' : '审批确认';
        const text = this.isReview ? '流程审批中...' : '工单审批中...';

        try {
          await this.$confirm(titleText, title);
        } catch (e) {
          return;
        }

        const loading = this.$loading({
          lock: true,
          fullscreen: true,
          text
        });
        this.approvalLoading = true;
        try {
          const api = this.isReview ? releaseByApproval : release;
          const data = await api([row.id]);
          if (data || data === 0) {
            this.$message.success('提交审核成功！');
          } else {
            this.$message.error('提交审核失败，请重新提交！');
          }
          await this.refreshSchedulingView();
          await this.reloadPlanSchedulingPlansByStatus();
        } catch (e) {
          this.$message.error(e.message || '提交审核失败，请重新提交！');
        } finally {
          this.approvalLoading = false;
          loading.close();
        }
      },

      async submitDotLineConfig() {
        if (!this.currentPlan?.id) {
          this.$message.warning('请先勾选一条计划后再确定');
          return;
        }
        if (!this.isOrderScheduling && this.isCurrentPlanReadonly) {
          this.$message.warning('当前计划状态下布局数据仅支持查看');
          return;
        }
        if (!this.taskList.length) {
          this.$message.warning('当前没有可提交的工艺配置');
          return;
        }
        if (!this.validateTaskList()) return;

        const payload = this.buildSavePayload(0);
        const loading = this.$loading({ lock: true, text: '保存中...' });
        try {
          await savePlanDotLine(payload);
          await this.refreshSchedulingView();
          if (!this.isOrderScheduling) {
            await this.reloadPlanSchedulingPlansByStatus({
              preserveCurrent: true
            });
          }
          if (this.currentPlan?.id) {
            await this.loadPlanDotLineDetail(this.currentPlan);
          }
          this.dotLineFullscreen = false;
          this.$message.success('保存成功');
        } catch (e) {
          this.$message.error(e.message || '保存失败');
        } finally {
          loading.close();
        }
      },
      async submitDotLineRow(row) {
        if (!this.currentPlan?.id) {
          this.$message.warning('请先勾选一条计划后再确定');
          return;
        }
        if (!this.isOrderScheduling && this.isCurrentPlanReadonly) {
          this.$message.warning('当前计划状态下布局数据仅支持查看');
          return;
        }
        if (!row) {
          this.$message.warning('请选择需要保存的工艺配置');
          return;
        }
        if (!this.validateTaskRow(row, { checkSequence: false })) return;

        const payload = this.buildSavePayload(this.isOrderScheduling ? 0 : 1);
        const loadingText = this.isOrderScheduling ? '保存中...' : '缓存中...';
        const successText = this.isOrderScheduling ? '保存成功' : '缓存成功';
        const errorText = this.isOrderScheduling ? '保存失败' : '缓存失败';
        const loading = this.$loading({ lock: true, text: loadingText });
        try {
          await savePlanDotLine(payload);
          await this.refreshSchedulingView();
          if (!this.isOrderScheduling) {
            await this.reloadPlanSchedulingPlansByStatus({
              preserveCurrent: true
            });
          }
          if (this.currentPlan?.id) {
            await this.loadPlanDotLineDetail(this.currentPlan);
          }
          this.$message.success(successText);
        } catch (e) {
          this.$message.error(e.message || errorText);
        } finally {
          loading.close();
        }
      },

      // Dialog lifecycle
      cancel() {
        if (this.orderDateReloadTimer) {
          clearTimeout(this.orderDateReloadTimer);
          this.orderDateReloadTimer = null;
        }
        this.form = {};
        this.entryReadonly = false;
        this.orderDetailReadonly = false;
        this.orderDetailActionMode = '';
        this.currentPlan = null;
        this.planRoutingPayload = null;
        this.taskList = [];
        this.selection = [];
        this.planPickerSelection = [];
        this.planPickerSelectionAll = [];
        this.planPickerTimeDimensionPlanType = '';
        this.planPickerWhere = createPlanPickerWhere();
        this.schedulingMode = 'plan';
        this.planTableStatusTab = this.getDefaultPlanTableStatusTab('plan');
        this.selectedWorkOrders = [];
        this.orderListLoader = null;
        this.columns = createSchedulingColumns();
        this.dotLineLoading = false;
        this.dotLineFullscreen = false;
        this.visible = false;
      }
    }
  };
</script>

<style lang="scss" scoped>
  .pane-box {
    padding: 20px 0;
  }

  .btnList {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 10px 0 12px;

    button {
      margin: 0;
    }
  }

  .scheduling-content {
    position: relative;
    margin-top: 6px;
    width: 100%;
    min-height: 0;
    padding: 0;
    box-sizing: border-box;
  }

  .form-wrapper {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    min-width: 0;
    overflow: hidden;
    border: 1px solid #d8e3ef;
    border-radius: 10px;
    background: linear-gradient(180deg, #ffffff 0%, #fafdff 100%);
    box-shadow: 0 10px 26px rgba(26, 39, 55, 0.08);
  }

  .form-wrapper::before,
  .plan-dot-line-detail::before {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    height: 2px;
    background: linear-gradient(90deg, #2489e8 0%, #14b8a6 60%, #7aa7ff 100%);
    content: '';
    pointer-events: none;
  }

  .plan-table-status-tabs {
    flex: 0 0 42px;
    height: 42px;
    padding: 3px 12px 0;
    box-sizing: border-box;
    border-bottom: 1px solid #e5edf6;
    background: linear-gradient(180deg, #f9fcff 0%, #f3f7fb 100%);
  }

  .plan-table-status-tabs ::v-deep {
    .el-tabs__header {
      margin: 0;
    }

    .el-tabs__nav-wrap::after {
      display: none;
    }

    .el-tabs__item {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      height: 36px;
      line-height: 1;
      margin-right: 8px;
      padding: 0 16px;
      border-radius: 7px 7px 0 0;
      color: #667085;
      font-size: 14px;
      font-weight: 600;
      transition: all 0.18s ease;
    }

    .el-tabs__item:not(.is-active):hover {
      color: #1683d8;
      background: rgba(31, 140, 235, 0.06);
    }

    .el-tabs__item.is-active {
      color: #0f75bd;
      background: #fff;
      box-shadow: 0 -1px 0 #dce8f4, 1px 0 0 #e5edf6, -1px 0 0 #e5edf6;
    }

    .el-tabs__active-bar {
      height: 2px;
      border-radius: 2px 2px 0 0;
      background: linear-gradient(90deg, #1f8ceb 0%, #16b8a6 100%);
    }
  }

  .form-wrapper ::v-deep .ele-pro-table {
    flex: 1 1 auto;
    height: calc(100% - 42px);
    min-height: 0;
    overflow: hidden;
    background: transparent;
  }

  .form-wrapper.is-order-scheduling ::v-deep .ele-pro-table,
  .form-wrapper.is-plan-scheduling ::v-deep .ele-pro-table {
    display: flex;
    flex-direction: column;
  }

  .form-wrapper.is-order-scheduling ::v-deep .ele-pro-table > .el-table {
    flex: 1 1 auto;
    height: calc(100% - 44px) !important;
    min-height: 0;
  }

  .form-wrapper.is-order-scheduling ::v-deep .ele-pro-table > .el-pagination {
    margin-top: auto !important;
  }

  .form-wrapper ::v-deep .el-table {
    color: #364152;
    border-color: #e5edf6;
    font-size: 14px;
  }

  .form-wrapper ::v-deep .el-table__body-wrapper {
    overflow-y: auto !important;
    overflow-x: auto;
    padding-bottom: 1px;
    box-sizing: border-box;
  }

  .form-wrapper ::v-deep .el-table th.el-table__cell {
    background: linear-gradient(180deg, #f8fbff 0%, #eef5fb 100%);
    color: #1f2d3d;
    font-weight: 600;
    position: sticky;
    top: 0;
    z-index: 2;
  }

  .form-wrapper ::v-deep .el-table td.el-table__cell,
  .form-wrapper ::v-deep .el-table th.el-table__cell {
    border-bottom-color: #e9eff6;
  }

  .form-wrapper ::v-deep .el-table td.el-table__cell {
    padding-top: 5px;
    padding-bottom: 5px;
  }

  .form-wrapper ::v-deep .el-table__row:hover > td.el-table__cell {
    background: #f5faff;
  }

  .form-wrapper ::v-deep .el-table__row.current-row > td.el-table__cell,
  .form-wrapper
    ::v-deep
    .el-table__row.el-table__row--striped.current-row
    > td.el-table__cell {
    background: #eaf5ff;
  }

  .form-wrapper ::v-deep .el-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 42px;
    height: 26px;
    padding: 0 10px;
    border: 1px solid #c8e0f4;
    border-radius: 6px;
    background: linear-gradient(180deg, #f6fbff 0%, #eaf5ff 100%);
    color: #0f75bd;
    font-weight: 600;
    line-height: 24px;
    transition: all 0.18s ease;
  }

  .form-wrapper ::v-deep .el-link:hover {
    border-color: #8ec6f4;
    background: linear-gradient(180deg, #eef8ff 0%, #dff0ff 100%);
    color: #0964a3;
  }

  .form-wrapper ::v-deep .el-checkbox__inner {
    border-color: #d8e1eb;
  }

  .form-wrapper ::v-deep .el-table__body-wrapper::-webkit-scrollbar,
  .form-wrapper ::v-deep .el-table__fixed-body-wrapper::-webkit-scrollbar {
    width: 8px;
    height: 8px;
  }

  .form-wrapper ::v-deep .el-table__body-wrapper::-webkit-scrollbar-thumb,
  .form-wrapper
    ::v-deep
    .el-table__fixed-body-wrapper::-webkit-scrollbar-thumb {
    border: 2px solid #fff;
    border-radius: 999px;
    background: #c9d4e2;
  }

  .form-wrapper ::v-deep .el-table__body-wrapper::-webkit-scrollbar-thumb:hover,
  .form-wrapper
    ::v-deep
    .el-table__fixed-body-wrapper::-webkit-scrollbar-thumb:hover {
    background: #aeb9c8;
  }

  .form-wrapper.is-plan-scheduling ::v-deep .ele-pro-table {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }

  .form-wrapper.is-plan-scheduling ::v-deep .ele-pro-table > .el-table {
    flex: 1 1 auto;
    height: calc(100% - 44px) !important;
    min-height: 0;
  }

  .form-wrapper.is-plan-scheduling ::v-deep .ele-pro-table > .el-pagination {
    margin-top: auto !important;
  }

  .form-wrapper.is-plan-scheduling ::v-deep .plan-scheduling-pagination {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 44px;
    height: 44px;
    min-height: 44px;
    padding: 0 10px;
    box-sizing: border-box;
    border-top: 1px solid #e5edf6;
    background: linear-gradient(180deg, #ffffff 0%, #f1f6fb 100%);
    box-shadow: 0 -3px 10px rgba(31, 45, 61, 0.04);
  }

  .form-wrapper.is-plan-scheduling
    ::v-deep
    .ele-pro-table
    > .el-pagination
    > * {
    margin-top: 0 !important;
  }

  .form-wrapper.is-order-scheduling ::v-deep .order-plan-pagination {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 44px;
    height: 44px;
    min-height: 44px;
    margin-top: 0 !important;
    padding: 0 10px;
    box-sizing: border-box;
    border-top: 1px solid #e5edf6;
    background: linear-gradient(180deg, #ffffff 0%, #f1f6fb 100%);
    box-shadow: 0 -3px 10px rgba(31, 45, 61, 0.04);
  }

  .form-wrapper.is-order-scheduling ::v-deep .order-plan-pagination > * {
    margin-top: 0 !important;
  }

  .plan-dot-line-side {
    width: 100%;
    height: 100%;
    overflow: hidden;
    padding-left: 10px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
  }

  .plan-dot-line-side.is-fullscreen {
    position: fixed;
    top: 24px;
    right: 24px;
    bottom: 24px;
    left: 24px;
    z-index: 3000;
    width: auto !important;
    height: auto !important;
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
    padding: 20px;
  }

  .plan-dot-line-detail {
    position: relative;
    display: flex;
    flex-direction: column;
    height: 100%;
    min-width: 0;
    overflow: hidden;
    border: 1px solid #d8e3ef;
    border-radius: 10px;
    background: linear-gradient(180deg, #ffffff 0%, #fafdff 100%);
    box-shadow: 0 10px 26px rgba(26, 39, 55, 0.08);
  }

  .plan-dot-line-scrollable {
    flex: 1;
    overflow: hidden;
    padding-bottom: 14px;
    box-sizing: border-box;
    min-height: 0;
  }

  .plan-dot-line-scrollable.is-order-dispatch-scroll {
    padding-bottom: 0;
  }

  .plan-dot-line-scrollable.is-plan-config-scroll {
    display: flex;
    flex-direction: column;
    padding-bottom: 0;
  }

  .plan-dot-line-scrollable.is-plan-config-scroll
    ::v-deep
    .factory-task-config-panel {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }

  .plan-dot-line-scrollable.is-plan-config-scroll
    ::v-deep
    .factory-task-config-panel
    .task-config-table-wrap {
    flex: 1 1 auto;
    height: 100%;
    min-height: 0;
  }

  @media (max-width: 1440px) {
    .scheduling-content {
      margin-top: 8px;
    }

    .plan-dot-line-side {
      padding-left: 8px;
    }

    .plan-dot-line-scrollable {
      padding-bottom: 12px;
    }
  }

  @media (max-width: 1180px) {
    .scheduling-content {
      height: auto !important;
    }

    .plan-dot-line-side {
      padding-left: 0;
      padding-top: 10px;
      min-height: 460px;
    }

    .form-wrapper {
      min-height: 260px;
    }

    .plan-dot-line-scrollable {
      overflow: auto;
    }
  }
</style>

<style lang="scss">
  .scheduling-dialog.el-dialog {
    width: calc(100vw - 10px) !important;
    max-width: none;
    height: calc(100vh - 10px);
    max-height: calc(100vh - 10px);
    margin-top: 5px !important;
    margin-bottom: 5px !important;
    border: 1px solid rgba(190, 205, 222, 0.85);
    border-radius: 12px;
    background: #eef4fa;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    box-shadow: 0 22px 64px rgba(15, 23, 42, 0.22);

    .el-dialog__header {
      flex: 0 0 auto;
      padding: 8px 16px;
      border-bottom: 1px solid #dce7f2;
      background: linear-gradient(180deg, #ffffff 0%, #f7fafc 100%);
    }

    .el-dialog__title {
      color: #1d2939;
      font-size: 16px;
      font-weight: 700;
      letter-spacing: 0;
    }

    .el-dialog__body {
      flex: 1 1 auto;
      display: flex;
      flex-direction: column;
      min-height: 0;
      padding: 6px 8px 6px;
      background: radial-gradient(
          circle at 12% 0,
          rgba(31, 140, 235, 0.08) 0,
          transparent 28%
        ),
        linear-gradient(90deg, rgba(31, 140, 235, 0.035) 1px, transparent 1px),
        linear-gradient(180deg, rgba(22, 184, 166, 0.025) 1px, transparent 1px),
        #eef4fa;
      background-size: auto, 24px 24px, 24px 24px;
      overflow: hidden;
    }

    .project-gantt {
      flex: 0 0 auto;
    }

    .scheduling-content {
      flex: 1 1 auto;
      height: auto !important;
      min-height: 0;
    }

    .el-dialog__footer {
      flex: 0 0 auto;
      padding: 5px 16px 6px;
      border-top: 1px solid #dce7f2;
      background: linear-gradient(180deg, #f9fbfd 0%, #f3f7fb 100%);
    }
  }

  @media (max-height: 760px) {
    .scheduling-dialog.el-dialog {
      .el-dialog__header {
        padding: 8px 14px;
      }

      .el-dialog__body {
        padding: 6px 8px;
      }

      .el-dialog__footer {
        padding: 4px 14px 5px;
      }
    }
  }

  @media (max-width: 1180px) {
    .scheduling-dialog.el-dialog {
      .el-dialog__body {
        overflow: auto;
      }

      .scheduling-content {
        flex: 0 0 auto;
        min-height: 720px;
      }
    }
  }
</style>
