<template>
  <ele-modal
    v-if="visible"
    custom-class="ele-dialog-form long-dialog-form plan-dot-line-dialog"
    :centered="true"
    :append-to-body="true"
    :visible.sync="dialogVisible"
    :close-on-click-modal="false"
    width="70%"
    :maxable="true"
    :resizable="true"
    :fullscreen.sync="modalFullscreen"
    :title="title"
    @close="onModalClose"
    @opened="onModalOpened"
  >
    <div class="plan-dot-line">
      <div v-if="currentPlan" class="plan-time-bar">
        <span class="plan-time-item">
          <span class="plan-time-label">计划开始时间：</span>
          <span class="plan-time-value">{{ planStartDisplay }}</span>
        </span>
        <span class="plan-time-item">
          <span class="plan-time-label">计划结束时间：</span>
          <span class="plan-time-value">{{ planEndDisplay }}</span>
        </span>

        <span class="plan-time-item">
          <span class="plan-time-label">计划数量：</span>
          <span class="plan-time-value">{{ planNumDisplay }}</span>
        </span>
      </div>
      <div class="top-route">
        <div class="panel-title">工艺路线</div>
        <el-empty
          v-if="taskList.length === 0"
          description="暂无工艺路线"
        ></el-empty>
        <el-steps
          v-else
          :active="routeStepsActive"
          space="20px"
          align-center
          finish-status="success"
          class="route-steps"
        >
          <el-step
            v-for="(item, index) in taskList"
            :key="`route-step-${item._taskKey}`"
            :title="routeStepTitle(item, index)"
            :class="{ active: routeDesIndex === index }"
            v-bind="routeErrorIndex === index ? { status: 'error' } : {}"
          ></el-step>
        </el-steps>
      </div>
      <div class="config-panel">
        <div class="panel-title">工艺配置</div>
        <el-empty
          v-if="taskList.length === 0"
          description="暂无工艺"
        ></el-empty>
        <div v-else class="task-config-table-wrap">
          <el-table
            :key="modalFullscreen ? 'fs' : 'nm'"
            ref="configTable"
            :height="modalFullscreen ? tableMaxHeight : undefined"
            :max-height="modalFullscreen ? undefined : tableMaxHeight"
            :data="taskList"
            border
            size="small"
            row-key="_taskKey"
            class="config-table"
          >
            <el-table-column
              type="index"
              label="序号"
              width="60"
              align="center"
              class-name="config-table-index-col"
            />
            <el-table-column
              label="工序名称"
              min-width="100"
              show-overflow-tooltip
              class-name="task-name-cell"
              align="center"
            >
              <template slot-scope="{ row, $index }">
                <span class="task-name-text">{{
                  row.taskName || row.name || `工艺${$index + 1}`
                }}</span>
              </template>
            </el-table-column>
            <el-table-column label="工序并行" min-width="92" align="center">
              <template slot-scope="{ row }">
                <el-select
                  v-model="row.isParallelism"
                  placeholder="工序并行"
                  class="config-table-control"
                  @change="handleParallelismChange(row)"
                >
                  <el-option label="否" :value="0" />
                  <el-option label="是" :value="1" />
                </el-select>
              </template>
            </el-table-column>
            <el-table-column label="执行模式" min-width="100" align="center">
              <template slot-scope="{ row }">
                <el-select
                  v-model="row.executionType"
                  placeholder="执行模式"
                  clearable
                  class="config-table-control"
                  @change="onExecutionTypeChange(row)"
                >
                  <el-option
                    v-for="opt in executionTypeOptions"
                    :key="opt.value"
                    :label="opt.label"
                    :value="opt.value"
                  />
                </el-select>
              </template>
            </el-table-column>
            <el-table-column label="执行对象" min-width="130" align="center">
              <template slot-scope="{ row }">
                <el-select
                  v-if="execTypeNum(row) === EXEC_TYPE.HOMEMADE"
                  v-model="row.executionTeamId"
                  placeholder="请选择"
                  clearable
                  filterable
                  class="config-table-control"
                  @change="onExecutionObjectChange(row)"
                >
                  <el-option
                    v-for="team in teamOptions"
                    :key="team.id"
                    :label="team.name"
                    :value="team.id"
                  />
                </el-select>
                <el-select
                  v-else-if="execTypeNum(row) === EXEC_TYPE.ENTRUST"
                  v-model="row.executionFactoryId"
                  placeholder="请选择"
                  clearable
                  filterable
                  class="config-table-control"
                  @change="onExecutionObjectChange(row)"
                >
                  <el-option
                    v-for="f in factoryList"
                    :key="f.value"
                    :label="f.label"
                    :value="f.value"
                  />
                </el-select>
                <el-select
                  v-else-if="execTypeNum(row) === EXEC_TYPE.OUTSOURCE"
                  :value="''"
                  placeholder="委外无需选择"
                  disabled
                  class="config-table-control"
                />
                <el-select
                  v-else
                  :value="''"
                  placeholder="请先选择执行模式"
                  disabled
                  class="config-table-control"
                />
              </template>
            </el-table-column>
            <el-table-column
              label="执行开始时间"
              min-width="168"
              align="center"
            >
              <template slot-scope="{ row }">
                <el-date-picker
                  v-model="row.executionStartTime"
                  type="datetime"
                  value-format="yyyy-MM-dd HH:mm:ss"
                  placeholder="执行开始时间"
                  clearable
                  class="config-table-control"
                  @change="handleTimeChange(row, 'executionStartTime')"
                />
              </template>
            </el-table-column>
            <el-table-column
              label="执行结束时间"
              min-width="168"
              align="center"
            >
              <template slot-scope="{ row }">
                <div
                  :class="{
                    'last-end-over-plan-wrap': isExecutionEndOverPlanEnd(row),
                    'end-over-delivery-wrap': isExecutionEndOverDelivery(row)
                  }"
                >
                  <el-date-picker
                    v-model="row.executionEndTime"
                    type="datetime"
                    value-format="yyyy-MM-dd HH:mm:ss"
                    placeholder="执行结束时间"
                    clearable
                    class="config-table-control"
                    @change="handleTimeChange(row, 'executionEndTime')"
                  />
                </div>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
    </div>
    <div slot="footer">
      <el-button size="mini" @click="onModalClose">取消</el-button>
      <el-button size="mini" type="warning" @click="handleClearAll"
        >一键清空</el-button
      >
      <el-button size="mini" type="success" @click="handleCache"
        >缓存</el-button
      >
      <el-button size="mini" type="primary" @click="handleSave">保存</el-button>
    </div>
  </ele-modal>
</template>

<script>
  import {
    getPlanDotLine,
    savePlanDotLine
  } from '@/api/productionPlan/planDotLine';
  import { getTaskListById } from '@/api/materialPlan';
  import { teamPage } from '@/api/mainData';
  import { getFactoryarea } from '@/api/saleOrder';

  const EXEC_TYPE = Object.freeze({ HOMEMADE: 0, ENTRUST: 1, OUTSOURCE: 2 });

  const EXEC_TYPE_OPTIONS = Object.freeze([
    { label: '自制', value: EXEC_TYPE.HOMEMADE },
    { label: '请托', value: EXEC_TYPE.ENTRUST },
    { label: '委外', value: EXEC_TYPE.OUTSOURCE }
  ]);

  function toSafeNumber(val) {
    if (val === '' || val == null) return undefined;
    const n = Number(val);
    return Number.isNaN(n) ? undefined : n;
  }

  function formatDateTime(val) {
    if (val == null || val === '') return '';
    const str = String(val).trim();
    if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(str)) return str;
    const d = new Date(val);
    if (Number.isNaN(d.getTime())) return '';
    const p = (n) => String(n).padStart(2, '0');
    return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(
      d.getHours()
    )}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
  }

  /** 比较两个时间字符串，结束 < 开始 则返回 true */
  function isEndBeforeStart(startStr, endStr) {
    if (!startStr || !endStr) return false;
    const s = new Date(startStr).getTime();
    const e = new Date(endStr).getTime();
    return !Number.isNaN(s) && !Number.isNaN(e) && e < s;
  }

  /** 工序结束时间是否晚于计划结束时间（用于最后一道工序标红与提醒） */
  function isEndAfterPlanEnd(executionEndStr, planEndStr) {
    if (!executionEndStr || !planEndStr) return false;
    const e = new Date(executionEndStr).getTime();
    const p = new Date(planEndStr).getTime();
    return !Number.isNaN(e) && !Number.isNaN(p) && e > p;
  }

  /** 计划交付时间是日期字段，比较时应以该日期当天结束为上限 */
  function isTimeAfterDeliveryDate(timeStr, deliveryDateStr) {
    if (!timeStr || !deliveryDateStr) return false;
    const time = new Date(timeStr).getTime();
    const deliveryDate = new Date(deliveryDateStr);
    if (Number.isNaN(time) || Number.isNaN(deliveryDate.getTime()))
      return false;
    deliveryDate.setHours(23, 59, 59, 999);
    return time > deliveryDate.getTime();
  }

  export default {
    data() {
      return {
        visible: false,
        dialogVisible: false,
        title: '计划布点',
        taskList: [],
        teamOptions: [],
        currentPlan: null,
        planRoutingPayload: null,
        executionTypeOptions: EXEC_TYPE_OPTIONS,
        /** 与 ele-modal 全屏按钮同步，用于表格高度适配 */
        modalFullscreen: false,
        /** 普通弹窗下动态限制表格最大高度，避免写死 420 */
        tableMaxHeight: 420,
        /** 工艺路线步骤 error 状态（-1 表示无） */
        routeErrorIndex: -1,
        factoryList: [],
        /** 模板中比对执行模式用 */
        EXEC_TYPE
      };
    },
    computed: {
      /** 当前激活步骤：首个未配置执行模式的工序；全部已配置则为步骤总数（全部 finish） */
      routeStepsActive() {
        const list = this.taskList;
        if (!list.length) return 0;
        for (let i = 0; i < list.length; i++) {
          if (!this.isRouteItemDone(list[i])) return i;
        }
        return list.length;
      },
      /** 用于 :class="active" 高亮，与 active 同步（全部完成时高亮最后一环） */
      routeDesIndex() {
        const list = this.taskList;
        if (!list.length) return -1;
        const a = this.routeStepsActive;
        return a >= list.length ? list.length - 1 : a;
      },
      /** 顶部展示：与列表行字段 startTime / endTime 一致（见 productionPlan/index.vue 列配置） */
      planStartDisplay() {
        const t = this.currentPlan?.startTime;
        return t ? formatDateTime(t) || String(t) : '—';
      },
      planEndDisplay() {
        const t = this.currentPlan?.endTime;
        return t ? formatDateTime(t) || String(t) : '—';
      },
      planNumDisplay() {
        const t =
          this.currentPlan?.productNum + this.currentPlan?.measuringUnit;
        return t ? t : '—';
      },
      /** 最后一道工序执行结束时间是否晚于计划结束时间 */
      lastProcessEndAfterPlanEnd() {
        const list = this.taskList;
        const planEnd = this.currentPlan?.endTime;
        if (!list.length || !planEnd) return false;
        const last = list[list.length - 1];
        if (!last?.executionEndTime) return false;
        return isEndAfterPlanEnd(last.executionEndTime, planEnd);
      }
    },
    watch: {
      modalFullscreen() {
        // 先算好高度，key 变化会重建 el-table，重建后再 doLayout
        this.calcTableHeight();
        this.$nextTick(() => {
          this.$refs.configTable?.doLayout?.();
          // 全屏动画结束后再修正一次
          setTimeout(() => {
            this.calcTableHeight();
            this.$nextTick(() => {
              this.$refs.configTable?.doLayout?.();
            });
          }, 400);
        });
      }
    },
    beforeDestroy() {
      window.removeEventListener('resize', this.calcTableHeight);
    },
    methods: {
      onModalOpened() {
        window.addEventListener('resize', this.calcTableHeight);
        this.$nextTick(() => {
          this.calcTableHeight();
        });
      },
      calcTableHeight() {
        if (this.modalFullscreen) {
          const tableEl = this.$refs.configTable?.$el;
          const footerEl = tableEl
            ?.closest('.el-dialog')
            ?.querySelector('.el-dialog__footer');
          const footerH = footerEl ? footerEl.offsetHeight : 52;
          const wrapEl = tableEl?.closest('.task-config-table-wrap');
          if (wrapEl) {
            const top = wrapEl.getBoundingClientRect().top;
            const vh = window.innerHeight;
            this.tableMaxHeight = Math.max(
              200,
              Math.floor(vh - top - footerH - 16)
            );
          } else {
            const vh = window.innerHeight || 800;
            this.tableMaxHeight = Math.max(200, vh - 260);
          }
        } else {
          const vh = window.innerHeight || 800;
          this.tableMaxHeight = Math.max(320, Math.floor(vh * 0.52));
        }
        this.$nextTick(() => {
          this.$refs.configTable?.doLayout?.();
        });
      },
      async open(data) {
        this.currentPlan = data || null;
        this.planRoutingPayload = null;
        await Promise.all([
          this.loadTeamOptions(),
          this.loadFactoryList(),
          this.loadTaskList()
        ]);
        this.visible = true;
        this.dialogVisible = true;
      },

      async loadFactoryList() {
        const par = { type: 1, size: 9999 };
        try {
          const res = await getFactoryarea(par);
          if (res.list?.length) {
            this.factoryList = res.list.map((el) => ({
              value: el.id,
              label: el.name
            }));
          } else {
            this.factoryList = [];
          }
        } catch {
          this.factoryList = [];
        }
      },

      async loadTaskList() {
        this.taskList = [];
        this.planRoutingPayload = null;
        if (!this.currentPlan?.id) return;

        let planData;
        try {
          planData = await getPlanDotLine({ planId: this.currentPlan.id });
        } catch (e) {
          this.$message.error(e.message || '获取工序布点信息失败');
          return;
        }

        this.planRoutingPayload = planData || {};
        const details = planData?.detailList;

        if (details?.length) {
          this.taskList = [...details]
            .sort((a, b) => (a.taskSort ?? 0) - (b.taskSort ?? 0))
            .map((item, i) => this.normalizeDetailRow(item, i));
          return;
        }

        if (!this.currentPlan.produceRoutingId) {
          this.$message.warning('暂无布点明细且缺少工艺路线，无法加载工序');
          return;
        }
        try {
          const list = await getTaskListById(this.currentPlan.produceRoutingId);
          this.taskList = (list || []).map((item, i) =>
            this.normalizeRoutingTaskRow(item, i)
          );
        } catch (e) {
          this.$message.error(e.message || '获取工序列表失败');
        }
      },

      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;
        return {
          ...item,
          _taskKey: item.id ?? `detail-${item.taskId ?? index}`,
          isParallelism: toSafeNumber(item.isParallelism) === 1 ? 1 : 0,
          executionStartTime: formatDateTime(item.executionStartTime),
          executionEndTime: formatDateTime(item.executionEndTime),
          executionType: execType,
          executionFactoryId: entrust
            ? item.executionFactoryId ?? item.executionTeamId ?? ''
            : '',
          executionFactoryName: entrust
            ? item.executionFactoryName ?? item.executionTeamName ?? ''
            : '',
          executionTeamId: homemade ? item.executionTeamId ?? '' : '',
          executionTeamLeader:
            outsource || entrust ? '' : item.executionTeamLeader ?? '',
          executionTeamLeaderId:
            outsource || entrust ? '' : item.executionTeamLeaderId ?? '',
          executionTeamName:
            outsource || entrust ? '' : item.executionTeamName ?? ''
        };
      },

      normalizeRoutingTaskRow(item, index) {
        const plan = this.currentPlan;
        const taskId = item.id;

        return {
          ...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: '',
          executionTeamLeader: '',
          executionTeamLeaderId: '',
          executionTeamName: '',
          executionStartTime: '',
          executionEndTime: '',
          executionFactoryId: '',
          executionFactoryName: ''
        };
      },

      execTypeNum(row) {
        return toSafeNumber(row.executionType);
      },

      clearExecutionTeamFields(row) {
        this.$set(row, 'executionTeamId', '');
        this.$set(row, 'executionTeamName', '');
        this.$set(row, 'executionTeamLeader', '');
        this.$set(row, 'executionTeamLeaderId', '');
        this.$set(row, 'executionFactoryId', '');
        this.$set(row, 'executionFactoryName', '');
      },

      onExecutionTypeChange(row) {
        this.clearExecutionTeamFields(row);
      },

      onExecutionObjectChange(row) {
        const t = this.execTypeNum(row);
        if (t === EXEC_TYPE.HOMEMADE) {
          const team = this.teamOptions.find(
            (x) => x.id === row.executionTeamId
          );
          row.executionTeamName = team?.name ?? '';
          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 = '';
        }
      },

      handleClearAll() {
        this.taskList.forEach((row) => {
          this.$set(row, 'executionType', EXEC_TYPE.HOMEMADE);
          this.$set(row, 'isParallelism', 0);
          this.clearExecutionTeamFields(row);
          this.$set(row, 'executionStartTime', '');
          this.$set(row, 'executionEndTime', '');
        });
        this.$message.success('已清空');
      },

      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,
              leaderUserName,
              leaderUserId
            })
          );
        } catch {
          this.teamOptions = [];
        }
      },

      isRouteItemDone(row) {
        return row.executionType != null && row.executionType !== '';
      },

      routeStepTitle(item, index) {
        return (
          item.taskTypeName || item.taskName || item.name || `工艺${index + 1}`
        );
      },

      getTaskLabel(item, index) {
        return (
          item?.taskTypeName ||
          item?.taskName ||
          item?.name ||
          (typeof index === 'number' ? `工艺${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
        );
      },

      isTaskParallel(row) {
        return toSafeNumber(row?.isParallelism) === 1;
      },

      getCurrentPlanStartTime() {
        return (
          this.currentPlan?.startTime ||
          this.currentPlan?.planStartTime ||
          this.currentPlan?.executionStartTime ||
          ''
        );
      },

      isExecutionStartBeforePlanStart(executionStartTime) {
        if (!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
        );
      },

      validateTaskSequence(row, options = {}) {
        const index = this.taskList.findIndex((item) => item === row);
        if (index < 0) {
          return true;
        }
        const prev = this.taskList[index - 1];
        const next = this.taskList[index + 1];
        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.routeErrorIndex = index;
          this.$message.warning(
            `${this.getTaskLabel(
              row,
              index
            )}执行开始时间不能小于上一道工序${this.getTaskLabel(
              prev,
              index - 1
            )}的执行结束时间`
          );
          if (resetKey) {
            this.$set(row, resetKey, '');
          }
          return false;
        }

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

        return true;
      },

      handleTimeChange(row, changeKey) {
        if (isEndBeforeStart(row.executionStartTime, row.executionEndTime)) {
          this.$message.warning('执行结束时间不能小于执行开始时间');
          this.$set(row, changeKey, '');
          return;
        }
        if (
          changeKey === 'executionStartTime' &&
          this.isTaskParallel(row) &&
          this.isExecutionStartBeforePlanStart(row.executionStartTime)
        ) {
          this.$message.warning('执行开始时间不能小于计划开始时间');
          this.$set(row, changeKey, '');
          return;
        }
        if (!this.validateTaskSequence(row, { resetKey: changeKey })) {
          return;
        }
        this.routeErrorIndex = -1;
        const deadline = this.currentPlan?.planDeliveryTime;
        if (
          changeKey === 'executionStartTime' &&
          deadline &&
          row.executionStartTime
        ) {
          if (isTimeAfterDeliveryDate(row.executionStartTime, deadline)) {
            this.$message.warning('时间不能大于计划交付时间');
            this.$set(row, changeKey, '');
            return;
          }
        }
        if (changeKey === 'executionEndTime' && row.executionEndTime) {
          const planEnd = this.currentPlan?.endTime;
          const overDelivery =
            !!deadline &&
            isTimeAfterDeliveryDate(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 (!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;
        return (
          !!deadline &&
          !!row?.executionEndTime &&
          isTimeAfterDeliveryDate(row.executionEndTime, deadline)
        );
      },

      /** 执行结束时间晚于计划结束时间（任意工序行标红，可保存） */
      isExecutionEndOverPlanEnd(row) {
        const planEnd = this.currentPlan?.endTime;
        return (
          !!planEnd &&
          !!row?.executionEndTime &&
          isEndAfterPlanEnd(row.executionEndTime, planEnd)
        );
      },

      validateTaskList() {
        this.routeErrorIndex = -1;
        const label = (t) => this.getTaskLabel(t);

        for (const item of this.taskList) {
          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.isTaskParallel(item) &&
            this.isExecutionStartBeforePlanStart(item.executionStartTime)
          ) {
            this.$message.warning(
              `${label(item)}执行开始时间不能小于计划开始时间`
            );
            return false;
          }

          const deadline = this.currentPlan?.planDeliveryTime;
          if (deadline) {
            const checkStart = (val, name) => {
              if (!val) return true;
              if (!isTimeAfterDeliveryDate(val, deadline)) return true;
              this.$message.warning(
                `${label(item)}${name}不能大于计划交付时间`
              );
              return false;
            };
            if (!checkStart(item.executionStartTime, '执行开始时间'))
              return false;
          }
        }
        for (const item of this.taskList) {
          if (!this.validateTaskSequence(item)) {
            return false;
          }
        }
        return true;
      },

      /** @param {0|1} isCache 缓存传 1，保存传 0 */
      buildSavePayload(isCache = 0) {
        const plan = this.currentPlan;
        const head = this.planRoutingPayload || {};

        const detailList = this.taskList.map((row) => {
          const execType = toSafeNumber(row.executionType) ?? 0;
          const detail = {
            executionEndTime: row.executionEndTime || undefined,
            executionStartTime: row.executionStartTime || undefined,
            executionTeamId:
              execType === EXEC_TYPE.HOMEMADE
                ? 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)
          };
          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
        };
      },

      async handleCache() {
        if (!this.validateTaskList()) return;
        const payload = this.buildSavePayload(1);
        const loading = this.$loading({ lock: true, text: '缓存中...' });
        try {
          await savePlanDotLine(payload);
          this.$emit('save', {
            plan: this.currentPlan,
            taskList: this.taskList,
            payload
          });
          this.$message.success('缓存成功');
          this.onModalClose();
        } catch (e) {
          this.$message.error(e.message || '缓存失败');
        } finally {
          loading.close();
        }
      },

      async handleSave() {
        if (!this.validateTaskList()) return;

        const payload = this.buildSavePayload(0);
        const loading = this.$loading({ lock: true, text: '保存中...' });
        try {
          await savePlanDotLine(payload);
          this.$emit('save', {
            plan: this.currentPlan,
            taskList: this.taskList,
            payload
          });
          this.$message.success('保存成功');
          this.onModalClose();
        } catch (e) {
          this.$message.error(e.message || '保存失败');
        } finally {
          loading.close();
        }
      },

      onModalClose() {
        window.removeEventListener('resize', this.calcTableHeight);
        this.modalFullscreen = false;
        this.visible = false;
        this.dialogVisible = false;
        this.taskList = [];
        this.currentPlan = null;
        this.planRoutingPayload = null;
        this.routeErrorIndex = -1;
        this.$emit('success');
      }
    }
  };
</script>

<style lang="scss" scoped>
  .plan-dot-line,
  .top-route,
  .config-panel {
    width: 100%;
    max-width: 100%;
    min-width: 0;
    box-sizing: border-box;
  }

  .plan-dot-line {
    min-height: 360px;
  }

  .plan-time-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 12px 28px;
    margin-bottom: 12px;
    padding: 10px 12px;
    border: 1px solid #ebeef5;
    border-radius: 4px;
    background: #fafafa;
    font-size: 14px;
  }

  .plan-time-item {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
  }

  .plan-time-label {
    color: #606266;
    white-space: nowrap;
  }

  .plan-time-value {
    color: #303133;
    font-weight: 500;
  }

  .last-end-over-plan-wrap,
  .end-over-delivery-wrap {
    width: 100%;
  }

  .last-end-over-plan-wrap ::v-deep .el-input__inner,
  .end-over-delivery-wrap ::v-deep .el-input__inner {
    color: #f56c6c;
    border-color: #f56c6c;
  }

  .last-end-over-plan-wrap ::v-deep .el-input__inner:focus,
  .end-over-delivery-wrap ::v-deep .el-input__inner:focus {
    border-color: #f56c6c;
  }

  .top-route,
  .config-panel {
    border: 1px solid #ebeef5;
    border-radius: 4px;
    padding: 12px;
    background: #fff;
  }

  /* 工艺路线区域随内容增高，不要出现纵向滚动条 */
  .top-route {
    overflow: visible;
  }

  .config-panel {
    margin-top: 12px;
  }

  .panel-title {
    font-size: 14px;
    font-weight: 600;
    margin-bottom: 10px;
  }

  .route-steps {
    width: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: 8px;
    box-sizing: content-box;
  }

  .route-steps ::v-deep {
    .el-steps {
      display: flex;
      flex-wrap: nowrap;
      align-items: flex-start;
    }
    .el-step {
      flex-shrink: 0;
    }
    .el-step__title {
      font-size: clamp(16px, 2.2vw, 16px);
      line-height: 1.35;
      max-width: 8em;
      margin-left: auto;
      margin-right: auto;
      white-space: normal;
      word-break: break-all;
    }
    .el-step__title:not(.is-error) {
      color: #157a2c;
    }
    .el-step.active .el-step__title:not(.is-error) {
      font-weight: 600;
    }
    .el-step__line {
      top: 14px;
    }
    /* 已完成步骤 success 色（默认绿）改为品牌绿 */
    .el-step__head.is-success {
      color: #157a2c;
      border-color: #157a2c;
    }
    .el-step__head.is-success .el-step__line {
      background-color: #157a2c;
    }
    .el-step__head.is-success .el-step__icon {
      color: #157a2c;
      border-color: #157a2c;
    }
    .el-step__head.is-success .el-step__line-inner {
      background-color: #157a2c;
      border-color: #157a2c;
    }
    /* 当前步骤 process：ele-admin 对 .is-text 使用主题色实心圆，需覆盖 background */
    .el-step__head.is-process {
      color: #157a2c;
      border-color: #157a2c;
    }
    .el-step__head.is-process .el-step__icon {
      color: #157a2c;
      border-color: #157a2c;
    }
    .el-step__head.is-process .el-step__icon.is-text {
      color: #fff;
      border-color: #157a2c;
      background: #157a2c;
    }
    .el-step__head.is-process .el-step__icon.is-text .el-step__icon-inner {
      color: #fff;
    }
    .el-step__description.is-process {
      color: #157a2c;
    }
    .el-step__icon {
      width: 30px;
      height: 30px;
    }
    .el-step__main {
      padding-top: 8px;
      margin-top: 0;
    }
    .el-step__description {
      margin-top: 0;
      padding-right: 0;
    }
  }

  @media (max-width: 576px) {
    .top-route,
    .config-panel {
      padding: 8px;
    }
  }

  .task-config-table-wrap {
    width: 100%;
  }

  .config-table ::v-deep {
    /* 序号列：column 上的 style 无效，需 class-name + td 内 .cell */
    td.config-table-index-col .cell {
      font-size: 14px !important;
    }
    .el-table td > .cell {
      font-size: 14px;
      text-align: center;
    }
    .el-table__body .cell {
      padding-left: 6px;
      padding-right: 6px;
    }
    td.task-name-cell .task-name-text {
      font-size: 14px;
      line-height: 1.4;
    }
    /* 与工序名称一致：覆盖 el-select / date-editor 的 small 字号 */
    .config-table-control .el-input__inner {
      font-size: 14px;
    }
    .config-table-control {
      width: 100% !important;
    }
    .config-table-control.el-date-editor {
      max-width: 100%;
    }
  }
</style>

<!-- 弹窗 append-to-body 后表头不在带 scoped 的节点链上，需用弹窗 class 单独写表头 -->
<style lang="scss">
  .plan-dot-line-dialog {
    .task-config-table-wrap
      .el-table__header-wrapper
      th.el-table__cell
      > .cell {
      font-size: 15px !important;
      line-height: 1.4 !important;
    }

    .top-route {
      overflow: visible !important;
      max-height: none !important;
    }

    .route-steps .el-step__head.is-success {
      color: #157a2c;
      border-color: #157a2c;
    }
    .route-steps .el-step__head.is-success .el-step__line {
      background-color: #157a2c;
    }
    .route-steps .el-step__head.is-success .el-step__icon {
      color: #157a2c;
      border-color: #157a2c;
    }
    .route-steps .el-step__head.is-success .el-step__line-inner {
      background-color: #157a2c;
      border-color: #157a2c;
    }

    .route-steps .el-step__head.is-process {
      color: #157a2c;
      border-color: #157a2c;
    }
    .route-steps .el-step__head.is-process .el-step__icon {
      color: #157a2c;
      border-color: #157a2c;
    }
    .route-steps .el-step__head.is-process .el-step__icon.is-text {
      color: #fff;
      border-color: #157a2c;
      background: #157a2c;
    }
    .route-steps
      .el-step__head.is-process
      .el-step__icon.is-text
      .el-step__icon-inner {
      color: #fff;
    }
    .route-steps .el-step__title:not(.is-error) {
      color: #157a2c;
    }
    .route-steps .el-step__description.is-process {
      color: #157a2c;
    }
  }
</style>
