<template>
  <div class="mod-config">
    <el-form :inline="true" :model="searchData" label-position="top" class="search-form">
      <el-form-item label="BU">
        <el-select v-model="searchData.buNo" clearable filterable placeholder="全部" style="width: 130px" @change="handleSearchBuChange">
          <el-option
            v-for="item in userBuList"
            :key="`searchBu_${item.buNo}`"
            :label="item.buDesc ? `${normalizeBuNo(item.buNo, searchData.site)}-${item.buDesc}` : normalizeBuNo(item.buNo, searchData.site)"
            :value="normalizeBuNo(item.buNo, searchData.site)"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="项目编码">
        <el-input v-model="searchData.projectNo" clearable placeholder="请输入项目编码" style="width: 140px" />
      </el-form-item>
      <el-form-item label="项目描述">
        <el-input v-model="searchData.projectDesc" clearable placeholder="请输入项目描述" style="width: 160px" />
      </el-form-item>
      <el-form-item label="项目物料">
        <el-input v-model="searchData.testPartNo" clearable placeholder="请输入项目物料" style="width: 140px" />
      </el-form-item>
      <el-form-item label="料号描述">
        <el-input v-model="searchData.partDesc" clearable placeholder="请输入料号描述" style="width: 160px" />
      </el-form-item>
      <el-form-item label="客户编码">
        <el-input v-model="searchData.customerNo" clearable placeholder="请输入客户编码" style="width: 120px" />
      </el-form-item>
      <el-form-item label="打样单号">
        <el-input v-model="searchData.proofingNo" clearable placeholder="请输入打样单号" style="width: 150px" />
      </el-form-item>
      <el-form-item label="项目/物料同步">
        <el-select v-model="searchData.projectPartSyncFlag" clearable placeholder="全部" style="width: 120px">
          <el-option label="已同步" value="Y" />
          <el-option label="未同步" value="N" />
        </el-select>
      </el-form-item>
      <el-form-item label="打样单同步">
        <el-select v-model="searchData.proofSyncFlag" clearable placeholder="全部" style="width: 120px">
          <el-option label="已同步" value="Y" />
          <el-option label="未同步" value="N" />
        </el-select>
      </el-form-item>
      <el-form-item label="打样状态">
        <el-select v-model="searchData.proofingStatus" clearable placeholder="全部" style="width: 120px">
          <el-option label="进行中" value="进行中" />
          <el-option label="打样完成" value="打样完成" />
          <el-option label="草稿" value="草稿" />
        </el-select>
        <el-button type="primary" @click="getDataList('Y')">查询</el-button>
        <el-button @click="resetSearch">重置</el-button>
      </el-form-item>
      <el-form-item class="button-group-col">
        <el-row :gutter="8" type="flex" class="button-row" >
            <el-button type="primary" @click="openOneKeyDialog">一键创建</el-button>
<!--            <el-button type="warning" @click="toggleBatchEdit">{{ batchEditMode ? '取消修改' : '批量修改' }}</el-button>
            <el-button v-if="batchEditMode" type="success" @click="saveBatchPlan">保存修改</el-button>-->
          <el-button type="warning" @click="openCreateProofDialog()">新增打样</el-button>
          <el-button type="success" plain :loading="syncNpiLoading" @click="syncToNpi()">同步项目物料到NPI</el-button>
          <el-button type="success" plain :loading="syncProofLoading" @click="syncProofToNpi()">同步打样到NPI</el-button>
          <el-button type="info" v-if="isAuth('833001005:set')" plain @click="openBuProcessConfigDialog">事业部进度设置</el-button>
        </el-row>
      </el-form-item>
    </el-form>

    <el-table
      ref="trackingTable"
      :data="dataList"
      border class="data-table"
      stripe highlight-current-row
      height="700"
      v-loading="dataListLoading"
      @selection-change="selectionChange"
      @row-click="rowClick"
      style="width: 100%; margin-top: 8px"
    >
<!--      <el-table-column type="selection" width="45" />-->
      <el-table-column prop="projectNo" label="项目编码" width="90" show-overflow-tooltip>
        <template slot-scope="scope">
          <span :class="getProjectPartSyncTextClass(scope.row)">{{ scope.row.projectNo || '-' }}</span>
        </template>
      </el-table-column>
      <el-table-column prop="projectDesc" label="项目描述" width="120" show-overflow-tooltip>
        <template slot-scope="scope">
          <span :class="getProjectPartSyncTextClass(scope.row)">{{ scope.row.projectDesc || '-' }}</span>
        </template>
      </el-table-column>
      <el-table-column prop="testPartNo" label="项目物料" width="90" show-overflow-tooltip>
        <template slot-scope="scope">
          <span :class="getProjectPartSyncTextClass(scope.row)">{{ scope.row.testPartNo || '-' }}</span>
        </template>
      </el-table-column>
      <el-table-column prop="partDesc" label="物料描述" :fixed="'left'" width="150" show-overflow-tooltip>
        <template slot-scope="scope">
          <span :class="getProjectPartSyncTextClass(scope.row)">{{ scope.row.partDesc || '-' }}</span>
        </template>
      </el-table-column>
<!--      <el-table-column prop="customerNo" label="客户编码" width="110" show-overflow-tooltip />
      <el-table-column prop="customerDesc" label="客户名称" width="130" show-overflow-tooltip />-->
      <el-table-column prop="proofingNo" label="打样单号" width="90" show-overflow-tooltip>
        <template slot-scope="scope">
          <span :class="getProofSyncTextClass(scope.row)">{{ scope.row.proofingNo || '-' }}</span>
        </template>
      </el-table-column>
      <el-table-column prop="proofingStatus" label="打样状态" width="60">
        <template slot-scope="scope">
          <a :style="{color: (scope.row.proofingStatus === '打样完成' || scope.row.proofingStatus === '打样结束') ? '#67c23a' : '#046e97'}">
            {{ scope.row.proofingStatus || '进行中' }}</a>
        </template>
      </el-table-column>
      <el-table-column label="Tracking Status" align="center" width="190" show-overflow-tooltip>
        <template slot-scope="scope">
          <div :class="getTrackingStatusClass(scope.row)">
            {{ getTrackingStatusText(scope.row) }}
          </div>
        </template>
      </el-table-column>

      <el-table-column
        v-for="(item, processColumnIndex) in visibleProcessColumns"
        :key="`${item.code}_${item.sortNo || ''}_${processColumnIndex}`"
        :label="item.label"
        width="120"
        align="center"
        header-align="center"
      >
        <template slot-scope="scope">
          <div
            v-if="isProcessColumnVisibleForRow(scope.row, item)"
            :class="['process-cell', isProcessComplete(scope.row, item) ? 'is-complete' : '']"
          >
            <el-date-picker
              v-if="batchEditMode"
              v-model="scope.row[item.actualField]"
              type="date"
              value-format="yyyy-MM-dd"
              format="yyyy-MM-dd"
              placeholder="实际完成日期"
              style="width: 120px"
            />
            <div
              v-else
              class="process-content"
              @mouseenter="handleProcessTooltipMouseEnter(scope.row, item, $event)"
              @mousemove="handleProcessTooltipMouseMove($event)"
              @mouseleave="handleProcessTooltipMouseLeave"
            >
              <el-date-picker
                v-model="scope.row[item.actualField]"
                type="date"
                value-format="yyyy-MM-dd"
                format="yyyy-MM-dd"
                placeholder=""
                style="width: 122px"
                @change="handleProcessDateChange(scope.row, item, $event)"
              />
              <el-button
                class="process-complete-btn"
                type="success"
                size="mini"
                plain v-show="!isProcessComplete(scope.row, item)"
                :disabled="isProcessComplete(scope.row, item)"
                @click.stop="markProcessComplete(scope.row, item)"
              >√</el-button>
            </div>
          </div>
          <div v-else class="process-cell process-cell--masked"></div>
        </template>
      </el-table-column>

      <el-table-column label="Comments" width="220">
        <template slot-scope="scope">
          <div class="comments-cell" @dblclick.stop="startCommentsEdit(scope.row)">
            <template v-if="batchEditMode">
              <el-input
                v-model="scope.row.comments"
                class="comments-edit-input"
                size="mini"
                clearable
                placeholder="请输入Comments"
              />
            </template>
            <template v-else-if="isCommentsEditing(scope.row)">
              <el-input
                v-model="commentsDraft"
                v-focus-comments-input="isCommentsEditing(scope.row)"
                :ref="'commentsInput_' + scope.row.trackingId"
                :id="'commentsInput_' + scope.row.trackingId"
                class="comments-edit-input"
                size="mini"
                clearable
                placeholder="请输入,回车也可保存"
                @keydown.enter.native.prevent="saveCommentsEdit(scope.row)"
                @keyup.esc.native="cancelCommentsEdit"
                @blur="handleCommentsBlur(scope.row)"
              />
              <el-button
                class="comments-save-btn"
                type="success"
                size="mini"
                plain
                :loading="commentsSaveLoading"
                @mousedown.native.prevent
                @click.stop="saveCommentsEdit(scope.row)"
              >√</el-button>
            </template>
            <span v-else class="comments-text" :title="scope.row.comments || ''">{{ scope.row.comments || '-' }}</span>
          </div>
        </template>
      </el-table-column>
      <el-table-column label="操作" fixed="right" width="150" align="center">
        <template slot-scope="scope">
<!--          <a class="action-link" @click="syncToNpi(scope.row)">同步NPI</a>-->
<!--          <a class="action-link" @click="openCreateProofDialog(scope.row)">新增打样</a>-->
          <a @click="openOneKeyDialog('edit', scope.row)">修改</a>
          <a class="action-link delete-link" @click="deleteProof(scope.row)">删除</a>
          <a class="action-link end-link" v-if="scope.row.proofingNo && scope.row.proofingStatus !== '打样完成'" @click="finishProof(scope.row)">打样完成</a>
        </template>
      </el-table-column>
    </el-table>

    <el-pagination
      @size-change="sizeChangeHandle"
      @current-change="currentChangeHandle"
      :current-page="pageIndex"
      :page-sizes="[10, 20, 50, 100]"
      :page-size="pageSize"
      :total="totalPage"
      layout="total, sizes, prev, pager, next, jumper"
      style="margin-top: 12px; text-align: right"
    />

    <div
      v-show="processTooltip.visible"
      class="process-history-tooltip"
      :style="{
        left: `${processTooltip.left}px`,
        top: `${processTooltip.top}px`
      }"
    >{{ processTooltip.content }}</div>

    <el-dialog
      title="进度展示设置"
      :visible.sync="buProcessConfigDialogVisible"
      width="1050px"
      :close-on-click-modal="false"
      custom-class="bu-process-config-dialog"
    >
      <div class="bu-process-config-tip">
        <i class="el-icon-info"></i>
        <span>拖动可调整顺序（按序号从左到右），勾选控制可见列</span>
      </div>
      <el-table :data="buProcessConfigRows" border stripe max-height="560">
        <el-table-column prop="buDesc" label="BU" width="50" show-overflow-tooltip />
        <el-table-column label="可见进度列表" min-width="450" class-name="bu-process-list-column">
          <template slot-scope="scope">
            <el-checkbox-group
              v-model="scope.row.processCodes"
              class="bu-process-order-list"
              @change="handleBuProcessCodesChange(scope.row)"
            >
              <div
                v-for="(processCode, processCodeIndex) in scope.row.processOrderCodes"
                :key="`${scope.row.buNo}_${processCode}_${processCodeIndex}`"
                :class="['bu-process-order-item', isProcessOrderDraggingItem(scope.row, processCodeIndex) ? 'is-dragging' : '']"
                draggable="true"
                @dragstart="handleProcessOrderDragStart(scope.row, processCodeIndex, $event)"
                @dragover.prevent
                @dragenter.prevent
                @drop.prevent="handleProcessOrderDrop(scope.row, processCodeIndex, $event)"
                @dragend="handleProcessOrderDragEnd"
              >
                <span class="process-order-index">{{ processCodeIndex + 1 }}.</span>
                <i class="el-icon-rank process-order-handle"></i>
                <el-checkbox :label="processCode">{{ getProcessLabelByCode(processCode) }}</el-checkbox>
              </div>
            </el-checkbox-group>
          </template>
        </el-table-column>
      </el-table>

      <div slot="footer">
        <el-button @click="buProcessConfigDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="saveBuProcessConfig">保存设置</el-button>
      </div>
    </el-dialog>

    <el-dialog :title="oneKeyDialogMode === 'edit' ? '修改项目/物料/打样' : '一键创建项目/物料/打样'" :visible.sync="oneKeyDialogVisible" :close-on-click-modal="false" width="1000px">
      <el-form :model="oneKeyForm" label-position="top" class="one-key-grid-form">
        <el-row :gutter="16">
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('buNo')">
            <el-form-item label="BU" required>
              <el-select v-model="oneKeyForm.buNo" placeholder="请选择" style="width: 100%">
                <el-option v-for="item in userBuList" :key="item.buNo" :label="item.buDesc" :value="item.buNo"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('projectNo')">
            <el-form-item label="项目编码">
              <el-input v-model="oneKeyForm.projectNo" style="width: 100%" @blur="handleOneKeyProjectNoBlur"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12" v-show="isOneKeyCreateFieldVisible('projectDesc')">
            <el-form-item label="项目名称">
              <el-input v-model="oneKeyForm.projectDesc" style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
        </el-row>

        <el-row :gutter="16">
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('customerNo')">
            <el-form-item>
              <span slot="label" class="big-label">
                <a href="javascript:void(0)" @click="getBaseList(509)">客户编码</a>
                <a href="javascript:void(0)" @click="newCustomer">（新客户）</a>
              </span>
              <el-input v-model="oneKeyForm.customerNo" @blur="customerNoBlur" style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12" v-show="isOneKeyCreateFieldVisible('customerDesc')">
            <el-form-item label="客户名称">
              <el-input v-model="oneKeyForm.customerDesc" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cProjectRegion')">
            <el-form-item label="区域">
              <el-select v-model="oneKeyForm.cProjectRegion" placeholder="请选择" clearable style="width: 100%">
                <el-option
                  v-for="i in cProjectRegionList"
                  :key="i.cProjectRegion"
                  :label="i.cProjectRegion"
                  :value="i.cProjectRegion">
                </el-option>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>

        <el-row :gutter="16">
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('projectCreationDate')">
            <el-form-item label="立项日期">
              <el-date-picker
                v-model="oneKeyForm.projectCreationDate"
                type="date"
                value-format="yyyy-MM-dd"
                placeholder="选择日期"
                style="width: 100%">
              </el-date-picker>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('needDate')">
            <el-form-item label="预计完成日期">
              <el-date-picker
                v-model="oneKeyForm.needDate"
                type="date"
                value-format="yyyy-MM-dd"
                placeholder="选择日期"
                style="width: 100%">
              </el-date-picker>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('projectCategory')">
            <el-form-item label="项目分类">
              <el-select v-model="oneKeyForm.projectCategory" placeholder="请选择" clearable style="width: 100%">
                <el-option v-for="item in projectCategoryList" :key="item" :label="item" :value="item"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('priorityLevel')">
            <el-form-item label="优先级">
              <el-select v-model="oneKeyForm.priorityLevel" placeholder="请选择" clearable style="width: 100%">
                <el-option v-for="item in priorityList" :key="item" :label="item" :value="item"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>

        <el-row :gutter="16">
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('testPartNo')">
            <el-form-item label="项目料号">
              <el-input v-model="oneKeyForm.testPartNo" style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12" v-show="isOneKeyCreateFieldVisible('partDesc')">
            <el-form-item label="料号描述" required>
              <el-input v-model="oneKeyForm.partDesc" style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('partType')">
            <el-form-item label="料号状态">
              <el-select v-model="oneKeyForm.partType" placeholder="请选择" clearable style="width: 100%">
                <el-option v-for="item in partTypeList" :key="item" :label="item" :value="item"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>

        <el-row :gutter="16">
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('projectManager')">
            <el-form-item>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('projectManager')">PM/Sales</a></span>
              <el-input v-model="oneKeyForm.projectManagerName" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('projectOwner')">
            <el-form-item required>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('projectOwner')">PjM</a></span>
              <el-input v-model="oneKeyForm.projectOwnerName" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('engineer')">
            <el-form-item required>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('engineer')">Engineer</a></span>
              <el-input v-model="oneKeyForm.engineerName" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cManufactureEngineer')">
            <el-form-item>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cManufactureEngineer')">MFG</a></span>
              <el-input v-model="oneKeyForm.cManufactureEngineerName" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
        </el-row>

        <el-row :gutter="16">
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer6')">
            <el-form-item>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer6')">IQC</a></span>
              <el-input v-model="oneKeyForm.cQualityEngineer6Name" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer1')">
            <el-form-item>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer1')">IPQC-Lam/Pri/Etch/Slit</a></span>
              <el-input v-model="oneKeyForm.cQualityEngineer1Name" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer2')">
            <el-form-item>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer2')">IPQC-Converting</a></span>
              <el-input v-model="oneKeyForm.cQualityEngineer2Name" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer4')">
            <el-form-item>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer4')">SQE</a></span>
              <el-input v-model="oneKeyForm.cQualityEngineer4Name" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
        </el-row>

        <el-row :gutter="16">
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer3')">
            <el-form-item>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer3')">FQC1</a></span>
              <el-input v-model="oneKeyForm.cQualityEngineer3Name" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer5')">
            <el-form-item>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer5')">FQC2</a></span>
              <el-input v-model="oneKeyForm.cQualityEngineer5Name" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('ipqcHardTag')">
            <el-form-item>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('ipqcHardTag')">IPQC-Hardtag</a></span>
              <el-input v-model="oneKeyForm.ipqcHardTagName" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer7')">
            <el-form-item>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer7')">前道工程师</a></span>
              <el-input v-model="oneKeyForm.cQualityEngineer7Name" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
        </el-row>

        <el-row :gutter="16">
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('docEngineer')">
            <el-form-item>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('docEngineer')">文档工程师</a></span>
              <el-input v-model="oneKeyForm.docEngineerName" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('docEngineer2')">
            <el-form-item>
              <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('docEngineer2')">文档工程师2</a></span>
              <el-input v-model="oneKeyForm.docEngineer2Name" disabled style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('projectStatus')">
            <el-form-item label="项目状态">
              <el-select v-model="oneKeyForm.projectStatus" clearable style="width: 100%">
                <el-option v-for="item in projectStatusList" :key="item" :label="item" :value="item" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('projectPhase')">
            <el-form-item label="项目阶段">
              <el-select v-model="oneKeyForm.projectPhase" clearable style="width: 100%">
                <el-option
                  v-for="item in proofProjectPhaseList"
                  :key="item.projectPhase"
                  :label="item.projectPhase"
                  :value="item.projectPhase"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>

        <el-row :gutter="16">
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('proofingNo')">
            <el-form-item label="打样单号">
              <el-select
                v-model="oneKeyForm.proofingNo"
                placeholder="请选择打样单号"
                clearable
                filterable
                :loading="oneKeyProofingApplyLoading"
                :disabled="isBlankValue(oneKeyForm.projectNo)"
                style="width: 100%"
                @change="handleOneKeyProofingNoChange">
                <el-option
                  v-for="item in oneKeyProofingApplyOptions"
                  :key="item.applyNo"
                  :label="item.applyNo"
                  :value="item.applyNo"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('proofingStatus')">
            <el-form-item label="打样状态">
              <el-select v-model="oneKeyForm.proofingStatus" clearable style="width: 100%">
                <el-option label="草稿" value="草稿" />
                <el-option label="进行中" value="进行中" />
                <el-option label="打样完成" value="打样完成" />
                <el-option label="打样结束" value="打样结束" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('proofingNumber')">
            <el-form-item label="数量">
              <el-input v-model.number="oneKeyForm.proofingNumber" style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('planStartDate')">
            <el-form-item label="打样开始日期">
              <el-date-picker
                v-model="oneKeyForm.planStartDate"
                type="date"
                value-format="yyyy-MM-dd"
                placeholder="选择日期"
                style="width: 100%">
              </el-date-picker>
            </el-form-item>
          </el-col>
        </el-row>

        <el-row :gutter="16">
          <el-col :span="6" v-show="isOneKeyCreateFieldVisible('requiredDeliveryDate')">
            <el-form-item label="预计完成日期">
              <el-date-picker
                v-model="oneKeyForm.requiredDeliveryDate"
                type="date"
                value-format="yyyy-MM-dd"
                placeholder="选择日期"
                style="width: 100%">
              </el-date-picker>
            </el-form-item>
          </el-col>
          <el-col :span="isOneKeyCreateFieldVisible('requiredDeliveryDate') ? 18 : 24" v-show="isOneKeyCreateFieldVisible('remark')">
            <el-form-item label="备注">
              <el-input type="textarea" :rows="2" v-model="oneKeyForm.remark" style="width: 100%"></el-input>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <el-footer style="height: 40px; margin-top: 40px; text-align: center">
        <el-button @click="oneKeyDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="saveOneKeyLoading" @click="submitOneKey">{{ oneKeyDialogMode === 'edit' ? '保存修改' : '保存' }}</el-button>
      </el-footer>
    </el-dialog>

    <el-dialog title="新增打样" :visible.sync="proofDialogVisible" width="768px" :close-on-click-modal="false">
      <el-form :model="proofDialogData" label-position="top" class="proof-grid-form">
        <el-row :gutter="16">
          <el-col :span="6">
            <el-form-item label="项目编码">
              <el-input v-model="proofDialogData.projectNo" disabled style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="项目名称">
              <el-input v-model="proofDialogData.projectDesc" disabled style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="6">
            <el-form-item label="BU">
              <el-input v-model="proofDialogData.buNo" disabled style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>

        <el-row :gutter="16">
          <el-col :span="6">
            <el-form-item label="客户编码">
              <el-input v-model="proofDialogData.customerNo" disabled style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="客户名称">
              <el-input v-model="proofDialogData.customerDesc" disabled style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="6">
            <el-form-item label="项目料号">
              <el-input v-model="proofDialogData.testPartNo" disabled style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>

        <el-row :gutter="16">
          <el-col :span="12">
            <el-form-item label="料号描述">
              <el-input v-model="proofDialogData.partDesc" disabled style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="6">
            <el-form-item label="项目分类" required>
              <el-select v-model="proofDialogData.cProjectTypeDb" placeholder="请选择" clearable style="width: 100%">
                <el-option
                  v-for="item in cProjectTypeDbList"
                  :key="item.cProjectTypeDb"
                  :label="item.cProjectTypeDb"
                  :value="item.cProjectTypeDb">
                </el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="6">
            <el-form-item label="项目阶段" required>
              <el-select v-model="proofDialogData.projectPhase" placeholder="请选择" clearable style="width: 100%">
                <el-option
                  v-for="item in proofProjectPhaseList"
                  :key="item.projectPhase"
                  :label="item.projectPhase"
                  :value="item.projectPhase">
                </el-option>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>

        <el-row :gutter="16">
          <el-col :span="6">
            <el-form-item label="打样单号" required>
              <el-select
                v-model="proofDialogData.proofingNo"
                placeholder="请选择试验单号"
                clearable
                filterable
                :loading="proofDialogApplyLoading"
                :disabled="isBlankValue(proofDialogData.projectNo)"
                style="width: 100%"
                @change="handleProofDialogProofingNoChange">
                <el-option
                  v-for="item in proofDialogApplyOptions"
                  :key="item.applyNo"
                  :label="item.applyNo"
                  :value="item.applyNo"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="6">
            <el-form-item label="数量" required>
              <el-input v-model="proofDialogData.proofingNumber" style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="6">
            <el-form-item label="打样状态">
              <el-select v-model="proofDialogData.proofingStatus" placeholder="请选择" clearable style="width: 100%">
                <el-option label="草稿" value="草稿"></el-option>
                <el-option label="进行中" value="进行中"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="6">
            <el-form-item label="实际完成日期">
              <el-date-picker
                v-model="proofDialogData.actualityDeliveryDate"
                type="date"
                value-format="yyyy-MM-dd"
                placeholder="选择日期"
                style="width: 100%">
              </el-date-picker>
            </el-form-item>
          </el-col>
        </el-row>

        <el-row :gutter="16">
          <el-col :span="6">
            <el-form-item label="打样开始日期" required>
              <el-date-picker
                v-model="proofDialogData.planStartDate"
                type="date"
                value-format="yyyy-MM-dd"
                placeholder="选择日期"
                style="width: 100%">
              </el-date-picker>
            </el-form-item>
          </el-col>
          <el-col :span="6">
            <el-form-item label="预计完成日期" required>
              <el-date-picker
                v-model="proofDialogData.requiredDeliveryDate"
                type="date"
                value-format="yyyy-MM-dd"
                placeholder="选择日期"
                style="width: 100%">
              </el-date-picker>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="备注">
              <el-input v-model="proofDialogData.remark" style="width: 100%" type="textarea" :rows="2"></el-input>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>

      <el-footer style="height: 40px; margin-top: 40px; text-align: center">
        <el-button @click="proofDialogVisible = false">取 消</el-button>
        <el-button type="primary" :loading="proofSaveLoading" @click="saveProofRecord">保 存</el-button>
      </el-footer>
    </el-dialog>

    <el-dialog title="更新工序状态" :visible.sync="processDialogVisible" width="620px" :close-on-click-modal="false">
      <el-form :model="processForm" label-position="top" label-width="120px" :inline="true">
        <el-form-item label="工序">
          <el-input v-model="processForm.processName" disabled style="width: 180px" />
        </el-form-item>
        <el-form-item label="状态" required>
          <el-select v-model="processForm.status" style="width: 180px">
            <el-option label="未完成" value="未完成" />
            <el-option label="进行中" value="进行中" />
            <el-option label="已完成" value="已完成" />
          </el-select>
        </el-form-item>
        <el-form-item label="实际完成日期">
          <el-date-picker v-model="processForm.actualDate" type="date" value-format="yyyy-MM-dd" format="yyyy-MM-dd" style="width: 180px" />
        </el-form-item>
        <el-form-item label="备注">
          <el-input type="textarea" :rows="2" v-model="processForm.remark" style="width: 570px" />
        </el-form-item>
      </el-form>

      <el-table :data="processHistoryRows" border stripe height="220" style="margin-top: 32px">
        <el-table-column prop="changedDate" label="修改时间" width="120" />
        <el-table-column prop="changedBy" label="修改人" width="70" />
        <el-table-column prop="oldStatus" label="原状态" width="60" />
        <el-table-column prop="newStatus" label="新状态" width="60" />
        <el-table-column prop="oldActualDate" label="原实际" width="90" />
        <el-table-column prop="newActualDate" label="新实际" width="90" />
        <el-table-column prop="remark" label="备注" show-overflow-tooltip />
      </el-table>

      <div slot="footer">
        <el-button @click="processDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="saveProcessLoading" @click="submitProcessUpdate">保存</el-button>
      </div>
    </el-dialog>

    <el-dialog
      title="打样完成"
      :visible.sync="finishDialogVisible"
      width="260px"
      :close-on-click-modal="false"
      custom-class="finish-proof-dialog">
      <div class="finish-dialog-tip">
        <i class="el-icon-info"></i>
        <span>确认后将直接标记为<strong class="finish-tip-highlight">打样完成</strong></span>
      </div>
      <el-form :model="finishForm" label-width="100px" class="finish-form">
        <el-form-item label="实际完成日期" required>
          <el-date-picker
            v-model="finishForm.actualityDeliveryDate"
            type="date"
            value-format="yyyy-MM-dd"
            format="yyyy-MM-dd"
            placeholder="选择日期"
            style="width: 100%"
          />
        </el-form-item>
      </el-form>
      <div slot="footer">
        <el-button @click="finishDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="finishSaveLoading" @click="submitFinishProof">确认完成</el-button>
      </div>
    </el-dialog>

    <el-dialog :title="roleDialogTitle" :visible.sync="roleDialogVisible" width="559px" :close-on-click-modal="false">
      <el-form :inline="true" label-position="top" :model="roleSearchData" style="margin-left: 7px; margin-top: -5px;">
        <el-form-item label="用户账号">
          <el-input v-model="roleSearchData.username" clearable style="width: 110px"></el-input>
        </el-form-item>
        <el-form-item label="用户名">
          <el-input v-model="roleSearchData.userDisplay" clearable style="width: 110px"></el-input>
        </el-form-item>
        <el-form-item label="是否在用">
          <el-select filterable v-model="roleSearchData.active" clearable style="width: 140px">
            <el-option label="是" value="Y"></el-option>
            <el-option label="否" value="N"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label=" ">
          <el-button type="primary" style="padding: 3px 12px" @click="queryRoleDialogList">查询</el-button>
        </el-form-item>
      </el-form>
      <el-table
        :data="roleDialogList"
        stripe
        border
        highlight-current-row
        @row-dblclick="pickRole"
        style="width: 100%">
        <el-table-column prop="username" header-align="center" align="center" label="用户账号"></el-table-column>
        <el-table-column prop="userDisplay" header-align="center" align="center" label="用户名"></el-table-column>
        <el-table-column prop="active" header-align="center" align="center" label="是否在用"></el-table-column>
      </el-table>
      <el-footer style="height: 40px; margin-top: 10px; text-align: center">
        <el-button @click="roleDialogVisible = false">关闭</el-button>
      </el-footer>
    </el-dialog>

    <el-dialog title="新客户" :visible.sync="newCustomerFlag" width="348px" :close-on-click-modal="false" @close="closeNewCustomer">
      <el-form :inline="true" label-position="top" :model="newCustomerData" style="margin-left: 7px; margin-top: -5px;">
        <el-form-item label="客户名称">
          <el-input v-model="newCustomerData.customerDesc" style="width: 313px"></el-input>
        </el-form-item>
      </el-form>
      <el-footer style="height: 40px; margin-top: 10px; text-align: center">
        <el-button type="primary" @click="saveNewCustomer">保存</el-button>
        <el-button @click="newCustomerFlag = false">关闭</el-button>
      </el-footer>
    </el-dialog>

    <Chooselist ref="baseList" @getBaseData="getBaseData"></Chooselist>
  </div>
</template>

<script>
import {
  batchUpdateProofTrackingPlan,
  createProofTrackingRecord,
  deleteProofTracking,
  finishProofTracking,
  oneKeyCreateProofTracking,
  oneKeyUpdateProofTracking,
  queryProofTrackingBuProcessConfig,
  queryProofTrackingProcessColumns,
  queryProofTrackingProcessHistory,
  saveProofTrackingBuProcessConfig,
  searchProjectInfoTracking,
  searchProjectPartTracking,
  searchProofTracking,
  syncProofTracking,
  syncProjectPartTracking,
  updateProofTrackingComments,
  updateProofTrackingProcess
} from '@/api/sampleTracking/sampleTracking'
import { searchExpApplyList } from '@/api/erf/erf'
import { getCustomerNo, saveNewCustomer } from '@/api/eam/eamProject.js'
import { getSiteAndBuByUserName } from '@/api/eam/eam.js'
import {
  searchBusinessInfo,
  searchBusinessInfo1,
  searchBusinessInfo2,
  searchBusinessInfo3,
  searchBusinessInfo4,
  searchBusinessInfo5,
  searchBusinessInfo6,
  searchBusinessInfo7,
  searchBusinessInfo8
} from '@/api/factory/site.js'
import { queryCustomerList } from '@/api/customer/customer'
import Chooselist from '@/views/modules/common/Chooselist_eam'

function focusCommentsNativeInput (el, vnode) {
  const doFocus = () => {
    const componentInput = vnode && vnode.componentInstance && vnode.componentInstance.$refs
      ? vnode.componentInstance.$refs.input
      : null
    const inputEl = componentInput || (el && el.querySelector ? el.querySelector('input') : null)
    if (!inputEl || typeof inputEl.focus !== 'function') {
      return false
    }
    inputEl.focus()
    const textLength = inputEl.value ? String(inputEl.value).length : 0
    if (typeof inputEl.setSelectionRange === 'function') {
      inputEl.setSelectionRange(textLength, textLength)
    }
    return true
  }
  if (doFocus()) {
    return
  }
  setTimeout(doFocus, 0)
  setTimeout(doFocus, 50)
  setTimeout(doFocus, 100)
}

const DEFAULT_PROCESS_COLUMNS = [
  { label: 'Raw Material', code: 'rawMaterial', planField: 'rawMaterialPlanDate', actualField: 'rawMaterialActualDate', statusField: 'rawMaterialStatus' },
  { label: 'Cylinder/Cutter', code: 'cylinderCutter', planField: 'cylinderCutterPlanDate', actualField: 'cylinderCutterActualDate', statusField: 'cylinderCutterStatus' },
  { label: 'Lamination', code: 'lamination', planField: 'laminationPlanDate', actualField: 'laminationActualDate', statusField: 'laminationStatus' },
  { label: 'Printing', code: 'printing', planField: 'printingPlanDate', actualField: 'printingActualDate', statusField: 'printingStatus' },
  { label: 'Etching', code: 'etching', planField: 'etchingPlanDate', actualField: 'etchingActualDate', statusField: 'etchingStatus' },
  { label: 'Slitting&Packing', code: 'slittingPacking', planField: 'slittingPackingPlanDate', actualField: 'slittingPackingActualDate', statusField: 'slittingPackingStatus' },
  { label: 'Bonding', code: 'bonding', planField: 'bondingPlanDate', actualField: 'bondingActualDate', statusField: 'bondingStatus' },
  { label: 'CL60/CL822', code: 'cl60Cl822', planField: 'cl60Cl822PlanDate', actualField: 'cl60Cl822ActualDate', statusField: 'cl60Cl822Status' },
  { label: 'Spotting', code: 'spotting', planField: 'spottingPlanDate', actualField: 'spottingActualDate', statusField: 'spottingStatus' },
  { label: 'Encoding', code: 'encoding', planField: 'encodingPlanDate', actualField: 'encodingActualDate', statusField: 'encodingStatus' },
  { label: 'Die cut', code: 'dieCut', planField: 'dieCutPlanDate', actualField: 'dieCutActualDate', statusField: 'dieCutStatus' },
  { label: 'Inspection', code: 'inspection', planField: 'inspectionPlanDate', actualField: 'inspectionActualDate', statusField: 'inspectionStatus' },
  { label: 'Delivery&Package', code: 'deliveryPackage', planField: 'deliveryPackagePlanDate', actualField: 'deliveryPackageActualDate', statusField: 'deliveryPackageStatus' }
]

function cloneDefaultProcessColumns () {
  return DEFAULT_PROCESS_COLUMNS.map(item => Object.assign({}, item))
}

export default {
  name: 'ProjectProofTracking',
  directives: {
    focusCommentsInput: {
      inserted (el, binding, vnode) {
        if (binding.value) {
          focusCommentsNativeInput(el, vnode)
        }
      },
      componentUpdated (el, binding, vnode) {
        if (binding.value && !binding.oldValue) {
          focusCommentsNativeInput(el, vnode)
        }
      }
    }
  },
  components: {
    Chooselist
  },
  data () {
    return {
      searchData: {
        site: this.$store.state.user.site,
        userName: this.$store.state.user.name,
        projectNo: '',
        projectDesc: '',
        buNo: '',
        testPartNo: '',
        partDesc: '',
        customerNo: '',
        proofingNo: '',
        projectPartSyncFlag: '',
        proofSyncFlag: '',
        proofingStatus: ''
      },
      processColumns: cloneDefaultProcessColumns(),
      buProcessConfigDialogVisible: false,
      buProcessConfigRows: [],
      buProcessConfigMap: {},
      processOrderDragState: {
        buNo: '',
        fromIndex: -1
      },
      projectCategoryList: ['Low Risk', 'High Risk', 'Sustaining'],
      cProjectRegionList: [
        { cProjectRegion: 'Global' },
        { cProjectRegion: 'APAC' },
        { cProjectRegion: 'CHINA' },
        { cProjectRegion: 'EU' },
        { cProjectRegion: 'US' },
        { cProjectRegion: 'Mexico' },
        { cProjectRegion: 'Other' }
      ],
      priorityList: ['Low', 'Middle', 'High'],
      projectStatusList: ['草稿', '进行中', '已关闭'],
      partTypeList: ['Active', 'On hold', 'Cancel', 'EOL'],
      partStatusList: ['草稿', '进行中', '已量产', '正式量产'],
      cProjectTypeDbList: [
        { cProjectTypeDb: 'Sustaining' },
        { cProjectTypeDb: 'Low Risk' },
        { cProjectTypeDb: 'High Risk' }
      ],
      proofProjectPhaseList: [
        { projectPhase: 'Prototype' },
        { projectPhase: 'Alpha' },
        { projectPhase: 'Beta' },
        { projectPhase: 'Pre-launch' }
      ],
      // 一键创建字段白名单（仅 create 模式生效）：后续确认要隐藏哪些字段时，只改这里即可。
      oneKeyCreateVisibleFieldKeys: [
        'buNo',
        'projectNo',
        'projectDesc',
        'customerNo',
        'customerDesc',
        'cProjectRegion',
        'projectCreationDate',
        'needDate',
        'projectCategory',
        'priorityLevel',
        'testPartNo',
        'partDesc',
        'partType',
        'projectManager',
        'projectOwner',
        'engineer',
        'cManufactureEngineer',
        'cQualityEngineer6',
        'cQualityEngineer1',
        'cQualityEngineer2',
        'cQualityEngineer4',
        'cQualityEngineer3',
        'cQualityEngineer5',
        'ipqcHardTag',
        'cQualityEngineer7',
        'docEngineer',
        'docEngineer2',
        'projectStatus',
        'projectPhase',
        'proofingNo',
        'proofingStatus',
        'proofingNumber',
        'planStartDate',
        'requiredDeliveryDate',
        'remark'
      ],
      userBuList: [],
      tagNo: '',
      newCustomerFlag: false,
      newCustomerData: {
        customerDesc: ''
      },
      roleDialogVisible: false,
      roleDialogTitle: '',
      roleDialogField: '',
      roleDialogApiKey: '0',
      roleDialogList: [],
      roleSearchData: {
        site: this.$store.state.user.site,
        username: '',
        userDisplay: '',
        active: '',
        page: 1,
        limit: 50
      },
      roleConfig: {
        projectManager: { apiKey: '0', title: 'PM/Sales', nameField: 'projectManagerName' },
        projectOwner: { apiKey: '1', title: 'PjM', nameField: 'projectOwnerName' },
        cQualityEngineer1: { apiKey: '2', title: 'IPQC-Lam/Pri/Etch/Slit', nameField: 'cQualityEngineer1Name' },
        cQualityEngineer2: { apiKey: '3', title: 'IPQC-Converting', nameField: 'cQualityEngineer2Name' },
        cQualityEngineer3: { apiKey: '4', title: 'FQC1', nameField: 'cQualityEngineer3Name' },
        cManufactureEngineer: { apiKey: '5', title: 'MFG', nameField: 'cManufactureEngineerName' },
        engineer: { apiKey: '6', title: 'Engineer', nameField: 'engineerName' },
        cQualityEngineer4: { apiKey: '7', title: 'SQE', nameField: 'cQualityEngineer4Name' },
        cQualityEngineer5: { apiKey: '7', title: 'FQC2', nameField: 'cQualityEngineer5Name' },
        cQualityEngineer6: { apiKey: '7', title: 'IQC', nameField: 'cQualityEngineer6Name' },
        docEngineer: { apiKey: '7', title: '文档工程师', nameField: 'docEngineerName' },
        ipqcHardTag: { apiKey: '7', title: 'IPQC-Hardtag', nameField: 'ipqcHardTagName' },
        cQualityEngineer7: { apiKey: '7', title: '前道工程师', nameField: 'cQualityEngineer7Name' },
        docEngineer2: { apiKey: '8', title: '文档工程师2', nameField: 'docEngineer2Name' }
      },
      dataList: [],
      dataListLoading: false,
      syncNpiLoading: false,
      syncProofLoading: false,
      pageIndex: 1,
      pageSize: 20,
      totalPage: 0,
      currentRow: null,
      manualSelectedTrackingId: null,
      selectionRows: [],
      batchEditMode: false,
      processHistoryMap: {},
      processTooltipHoverKey: '',
      processTooltip: {
        visible: false,
        content: '',
        left: 0,
        top: 0
      },
      processHistoryRows: [],
      commentsEditingTrackingId: null,
      commentsDraft: '',
      commentsSaveLoading: false,

      oneKeyDialogVisible: false,
      oneKeyDialogMode: 'create',
      saveOneKeyLoading: false,
      oneKeyForm: {},
      oneKeyProofingApplyOptions: [],
      oneKeyProofingApplyLoading: false,
      oneKeyProofingApplyQueryToken: 0,

      proofDialogVisible: false,
      proofSaveLoading: false,
      proofDialogData: this.getDefaultProofDialogData(),
      proofDialogApplyOptions: [],
      proofDialogApplyLoading: false,
      proofDialogApplyQueryToken: 0,

      processDialogVisible: false,
      saveProcessLoading: false,
      processForm: {},

      finishDialogVisible: false,
      finishSaveLoading: false,
      finishForm: {
        trackingId: null,
        actualityDeliveryDate: ''
      }
    }
  },
  activated () {
    Promise.all([this.loadUserBuList(), this.queryProcessColumns()]).then(() => {
      this.queryBuProcessConfig()
    })
    this.getDataList()
  },
  computed: {
    visibleProcessColumns () {
      const searchBuNo = this.getSelectedSearchBuNo()
      // 指定了查询BU时，整表进度列按该BU配置展示。
      if (!searchBuNo) {
        return this.processColumns
      }
      const visibleCodes = this.getProcessVisibleCodesByBuNo(searchBuNo)
      if (!visibleCodes) {
        return this.processColumns
      }
      const visibleColumns = this.processColumns.filter(item => visibleCodes.indexOf(item.code) > -1)
      return visibleColumns.length > 0 ? visibleColumns : this.processColumns
    }
  },
  methods: {
    loadUserBuList () {
      if (this.userBuList.length > 0) {
        this.ensureBuProcessConfigRows()
        return Promise.resolve(this.userBuList)
      }
      const params = { username: this.$store.state.user.name }
      return getSiteAndBuByUserName(params).then(({ data }) => {
        if (data && data.code === 0) {
          const excludedBuNoList = ['3_02-Hardtag', '4_04-MHM', '5_05-Alpha']
          this.userBuList = (data.rows || []).filter(item => {
            const buNo = item && item.buNo ? String(item.buNo).trim() : ''
            return excludedBuNoList.indexOf(buNo) === -1
          })
          this.ensureBuProcessConfigRows()
        }
        return this.userBuList
      }).catch(() => {
        this.userBuList = []
        this.ensureBuProcessConfigRows()
        return []
      })
    },
    queryProcessColumns () {
      const selectedBuNo = this.getSelectedSearchBuNo()
      const queryBuNo = selectedBuNo || '*'
      // 列顺序来源规则：
      // 1) 未选择查询 BU：固定按 bu='*' 读取默认工序顺序；
      // 2) 已选择查询 BU：按该 BU 读取；若无配置由后端回退到 bu='*'。
      const inData = {
        site: this.$store.state.user.site,
        userName: this.$store.state.user.name,
        buNo: queryBuNo
      }
      return queryProofTrackingProcessColumns(inData).then(({ data }) => {
        if (!(data && data.code === 0)) {
          this.$message.error((data && data.msg) || '加载工序列配置失败，已使用默认配置')
          this.processColumns = cloneDefaultProcessColumns()
          this.ensureBuProcessConfigRows()
          this.refreshTrackingTableLayout()
          return this.processColumns
        }
        const rows = Array.isArray(data.rows) ? data.rows : []
        // 防御性排序：即使后端 SQL 已 ORDER BY sort_no，前端仍按 sortNo 二次排序，确保展示稳定。
        const sortedRows = rows
          .map((item, index) => {
            const parsedSortNo = parseInt(item && item.sortNo, 10)
            return {
              item: item,
              sourceIndex: index,
              sortNo: Number.isNaN(parsedSortNo) ? Number.MAX_SAFE_INTEGER : parsedSortNo
            }
          })
          .sort((a, b) => {
            if (a.sortNo !== b.sortNo) {
              return a.sortNo - b.sortNo
            }
            return a.sourceIndex - b.sourceIndex
          })
        const nextColumns = []
        const usedCodeMap = {}
        sortedRows.forEach(({ item, sortNo }) => {
          if (!item) {
            return
          }
          const code = item.code == null ? '' : String(item.code).trim()
          const label = item.label == null ? '' : String(item.label).trim()
          const planField = item.planField == null ? '' : String(item.planField).trim()
          const actualField = item.actualField == null ? '' : String(item.actualField).trim()
          const statusField = item.statusField == null ? '' : String(item.statusField).trim()
          if (!code || !label || !planField || !actualField || !statusField || usedCodeMap[code]) {
            return
          }
          usedCodeMap[code] = true
          nextColumns.push({
            label: label,
            code: code,
            planField: planField,
            actualField: actualField,
            statusField: statusField,
            sortNo: sortNo === Number.MAX_SAFE_INTEGER ? null : sortNo
          })
        })
        // 保障切换期可用性：当表里无有效配置时继续使用前端兜底工序定义。
        this.processColumns = nextColumns.length > 0 ? nextColumns : cloneDefaultProcessColumns()
        this.syncDataListProcessValues()
        this.ensureBuProcessConfigRows()
        this.refreshTrackingTableLayout()
        return this.processColumns
      }).catch(() => {
        this.$message.error('加载工序列配置异常，已使用默认配置')
        this.processColumns = cloneDefaultProcessColumns()
        this.syncDataListProcessValues()
        this.ensureBuProcessConfigRows()
        this.refreshTrackingTableLayout()
        return this.processColumns
      })
    },
    getRowProcessValue (row, processCode) {
      if (!row || !processCode) {
        return null
      }
      const processMap = row.processValueMap && typeof row.processValueMap === 'object'
        ? row.processValueMap
        : null
      if (!processMap) {
        return null
      }
      return processMap[processCode] || null
    },
    applyProcessValuesToRow (row) {
      if (!row || !this.processColumns || this.processColumns.length === 0) {
        return
      }
      this.processColumns.forEach(item => {
        if (!item) {
          return
        }
        const processValue = this.getRowProcessValue(row, item.code)
        const planDate = processValue ? this.formatDate(processValue.planDate) : ''
        const actualDate = processValue ? this.formatDate(processValue.actualDate) : ''
        const status = processValue && processValue.status ? String(processValue.status).trim() : ''
        this.$set(row, item.planField, planDate)
        this.$set(row, item.actualField, actualDate)
        this.$set(row, item.statusField, status)
      })
    },
    syncDataListProcessValues () {
      if (!Array.isArray(this.dataList) || this.dataList.length === 0) {
        return
      }
      this.dataList.forEach(row => {
        this.applyProcessValuesToRow(row)
      })
    },
    updateRowProcessValue (row, processCol, status, actualDate) {
      if (!row || !processCol) {
        return
      }
      if (!row.processValueMap || typeof row.processValueMap !== 'object') {
        this.$set(row, 'processValueMap', {})
      }
      const prevValue = this.getRowProcessValue(row, processCol.code) || {}
      this.$set(row.processValueMap, processCol.code, Object.assign({}, prevValue, {
        processCode: processCol.code,
        status: status,
        actualDate: actualDate || null
      }))
      this.$set(row, processCol.statusField, status)
      this.$set(row, processCol.actualField, actualDate)
    },
    getAllProcessCodes () {
      return this.processColumns.map(item => item.code)
    },
    getProcessLabelByCode (processCode) {
      if (!processCode) {
        return ''
      }
      const matched = this.processColumns.find(item => item && item.code === processCode)
      if (matched && matched.label) {
        return matched.label
      }
      const defaultMatched = DEFAULT_PROCESS_COLUMNS.find(item => item && item.code === processCode)
      return defaultMatched && defaultMatched.label ? defaultMatched.label : processCode
    },
    getValidProcessOrderCodes (codes) {
      // 顺序列表不能因为“当前 BU 不可见”而丢项：先保留后端返回顺序，再补齐当前已知工序。
      const orderedCodes = []
      if (Array.isArray(codes)) {
        codes.forEach(code => {
          const codeVal = code == null ? '' : String(code).trim()
          if (!codeVal || orderedCodes.indexOf(codeVal) > -1) {
            return
          }
          orderedCodes.push(codeVal)
        })
      }
      const allCodes = this.getAllProcessCodes()
      allCodes.forEach(code => {
        if (orderedCodes.indexOf(code) === -1) {
          orderedCodes.push(code)
        }
      })
      return orderedCodes
    },
    normalizeBuNo (buNo, site) {
      const rawBuNo = buNo == null ? '' : String(buNo).trim()
      if (!rawBuNo) {
        return ''
      }
      const splitIdx = rawBuNo.indexOf('_')
      if (splitIdx < 1 || splitIdx >= rawBuNo.length - 1) {
        return rawBuNo
      }
      // BU统一按“去掉site前缀”展示与保存，例如 2_01-Label -> 01-Label。
      return rawBuNo.substring(splitIdx + 1)
    },
    getValidProcessCodes (codes, allowedCodes) {
      if (!Array.isArray(codes) || codes.length === 0) {
        return []
      }
      const allCodes = Array.isArray(allowedCodes) && allowedCodes.length > 0
        ? allowedCodes
          .map(code => (code == null ? '' : String(code).trim()))
          .filter(code => !!code)
        : this.getAllProcessCodes()
      const normalizedCodes = []
      codes.forEach(code => {
        const codeVal = code == null ? '' : String(code).trim()
        if (!codeVal || allCodes.indexOf(codeVal) === -1) {
          return
        }
        if (normalizedCodes.indexOf(codeVal) === -1) {
          normalizedCodes.push(codeVal)
        }
      })
      return normalizedCodes
    },
    getBuProcessConfigEntry (buNo, site) {
      const normalizedBuNo = this.normalizeBuNo(buNo, site)
      if (!normalizedBuNo) {
        return null
      }
      const config = this.buProcessConfigMap[normalizedBuNo]
      if (config) {
        return { key: normalizedBuNo, config: config }
      }
      return null
    },
    getSelectedSearchBuNo () {
      return this.normalizeBuNo(this.searchData.buNo, this.searchData.site)
    },
    handleSearchBuChange () {
      // BU筛选变化（含清空）后立即重新查询，并回到第一页。
      this.queryProcessColumns().then(() => {
        this.getDataList('Y')
      })
    },
    getProcessVisibleCodesByBuNo (buNo, site) {
      const configEntry = this.getBuProcessConfigEntry(buNo, site)
      const config = configEntry ? configEntry.config : null
      // 没有配置或配置为全部显示时，返回 null 表示该 BU 工序列不过滤。
      if (!config || config.showAll !== false) {
        return null
      }
      const validCodes = this.getValidProcessCodes(config.processCodes)
      if (validCodes.length === 0 || validCodes.length >= this.processColumns.length) {
        return null
      }
      return validCodes
    },
    isProcessColumnVisibleForRow (row, processCol) {
      if (!row || !processCol) {
        return true
      }
      // 已指定查询BU时，列集合已经按查询BU过滤，不再按行二次隐藏。
      if (this.getSelectedSearchBuNo()) {
        return true
      }
      const visibleCodes = this.getProcessVisibleCodesByBuNo(row.buNo, row.site)
      if (!visibleCodes) {
        return true
      }
      return visibleCodes.indexOf(processCol.code) > -1
    },
    queryBuProcessConfig () {
      const inData = {
        site: this.$store.state.user.site,
        userName: this.$store.state.user.name
      }
      queryProofTrackingBuProcessConfig(inData).then(({ data }) => {
        if (!(data && data.code === 0)) {
          this.$message.error((data && data.msg) || '加载事业部进度展示配置失败')
          return
        }
        const rows = data.rows || []
        const nextMap = {}
        rows.forEach(item => {
          const buNo = this.normalizeBuNo(item && item.buNo ? item.buNo : '', this.$store.state.user.site)
          if (!buNo) {
            return
          }
          const processOrderCodes = this.getValidProcessOrderCodes(item.processOrderCodes)
          const processCodes = this.getValidProcessCodes(item.processCodes, processOrderCodes)
          const showAll = item && item.showAll === true
            ? true
            : processCodes.length >= processOrderCodes.length
          const selectedCodeMap = {}
          processCodes.forEach(code => {
            selectedCodeMap[code] = true
          })
          nextMap[buNo] = {
            showAll: showAll,
            processOrderCodes: processOrderCodes,
            processCodes: showAll ? processOrderCodes.slice() : processOrderCodes.filter(code => selectedCodeMap[code])
          }
        })
        this.buProcessConfigMap = nextMap
        this.ensureBuProcessConfigRows()
        this.refreshTrackingTableLayout()
      }).catch(() => {
        this.$message.error('加载事业部进度展示配置异常')
      })
    },
    ensureBuProcessConfigRows () {
      const allCodes = this.getAllProcessCodes()
      const rows = []
      const usedBuMap = {}
      const appendRow = (buNo, buDesc, site) => {
        const key = this.normalizeBuNo(buNo, site || this.$store.state.user.site)
        if (!key || usedBuMap[key]) {
          return
        }
        usedBuMap[key] = true
        const cfg = this.buProcessConfigMap[key]
        const row = {
          buNo: key,
          buDesc: buDesc || key,
          processOrderCodes: allCodes.slice(),
          processCodes: allCodes.slice()
        }
        if (cfg) {
          const validOrderCodes = this.getValidProcessOrderCodes(cfg.processOrderCodes)
          row.processOrderCodes = validOrderCodes
          if (cfg.showAll === false) {
            const validCodes = this.getValidProcessCodes(cfg.processCodes, validOrderCodes)
            if (validCodes.length > 0 && validCodes.length < validOrderCodes.length) {
              const selectedCodeMap = {}
              validCodes.forEach(code => {
                selectedCodeMap[code] = true
              })
              row.processCodes = validOrderCodes.filter(code => selectedCodeMap[code])
            }
          } else {
            row.processCodes = validOrderCodes.slice()
          }
        }
        rows.push(row)
      }
      this.userBuList.forEach(item => {
        if (!item || !item.buNo) {
          return
        }
        appendRow(item.buNo, item.buDesc || this.normalizeBuNo(item.buNo, this.$store.state.user.site), this.$store.state.user.site)
      })
      Object.keys(this.buProcessConfigMap).forEach(buNo => {
        appendRow(buNo, buNo, this.$store.state.user.site)
      })
      this.buProcessConfigRows = rows
    },
    openBuProcessConfigDialog () {
      this.ensureBuProcessConfigRows()
      this.processOrderDragState = {
        buNo: '',
        fromIndex: -1
      }
      this.buProcessConfigDialogVisible = true
    },
    refreshTrackingTableLayout () {
      this.$nextTick(() => {
        const tableRef = this.$refs.trackingTable
        if (tableRef && tableRef.doLayout) {
          tableRef.doLayout()
        }
      })
    },
    handleBuProcessCodesChange (row) {
      if (!row) {
        return
      }
      const validOrderCodes = this.getValidProcessOrderCodes(row.processOrderCodes)
      const validCodes = this.getValidProcessCodes(row.processCodes, validOrderCodes)
      const selectedCodeMap = {}
      validCodes.forEach(code => {
        selectedCodeMap[code] = true
      })
      row.processOrderCodes = validOrderCodes
      row.processCodes = validOrderCodes.filter(code => selectedCodeMap[code])
    },
    isProcessOrderDraggingItem (row, index) {
      if (!row || index == null) {
        return false
      }
      return this.processOrderDragState.buNo === row.buNo && this.processOrderDragState.fromIndex === index
    },
    handleProcessOrderDragStart (row, index, event) {
      if (!row || index == null || index < 0) {
        return
      }
      this.processOrderDragState = {
        buNo: row.buNo || '',
        fromIndex: index
      }
      if (event && event.dataTransfer) {
        event.dataTransfer.effectAllowed = 'move'
      }
    },
    handleProcessOrderDrop (row, targetIndex, event) {
      if (event && event.dataTransfer) {
        event.dataTransfer.dropEffect = 'move'
      }
      if (!row || targetIndex == null || targetIndex < 0) {
        this.handleProcessOrderDragEnd()
        return
      }
      const dragBuNo = this.processOrderDragState.buNo
      const fromIndex = Number(this.processOrderDragState.fromIndex)
      if (!dragBuNo || dragBuNo !== row.buNo || Number.isNaN(fromIndex) || fromIndex < 0) {
        this.handleProcessOrderDragEnd()
        return
      }
      const validOrderCodes = this.getValidProcessOrderCodes(row.processOrderCodes)
      if (fromIndex >= validOrderCodes.length || targetIndex >= validOrderCodes.length) {
        this.handleProcessOrderDragEnd()
        return
      }
      if (fromIndex === targetIndex) {
        this.handleProcessOrderDragEnd()
        return
      }
      const movedCode = validOrderCodes.splice(fromIndex, 1)[0]
      const insertIndex = targetIndex
      validOrderCodes.splice(insertIndex, 0, movedCode)
      row.processOrderCodes = validOrderCodes
      this.handleBuProcessCodesChange(row)
      this.handleProcessOrderDragEnd()
    },
    handleProcessOrderDragEnd () {
      this.processOrderDragState = {
        buNo: '',
        fromIndex: -1
      }
    },
    saveBuProcessConfig () {
      if (!this.buProcessConfigRows || this.buProcessConfigRows.length === 0) {
        this.$message.warning('暂无可配置的事业部')
        return
      }
      const nextMap = {}
      const submitRows = []
      for (let i = 0; i < this.buProcessConfigRows.length; i++) {
        const row = this.buProcessConfigRows[i]
        const buNo = this.normalizeBuNo(row && row.buNo ? row.buNo : '', this.$store.state.user.site)
        if (!buNo) {
          continue
        }
        const validOrderCodes = this.getValidProcessOrderCodes(row.processOrderCodes)
        const selectedCodeMap = {}
        this.getValidProcessCodes(row.processCodes, validOrderCodes).forEach(code => {
          selectedCodeMap[code] = true
        })
        const validCodes = validOrderCodes.filter(code => selectedCodeMap[code])
        if (validCodes.length === 0) {
          this.$message.warning(`事业部【${row.buDesc || buNo}】至少需要选择一个可见进度`)
          return
        }
        const showAll = validCodes.length >= validOrderCodes.length
        nextMap[buNo] = {
          showAll: showAll,
          processOrderCodes: validOrderCodes,
          processCodes: showAll ? validOrderCodes.slice() : validCodes
        }
        submitRows.push({
          buNo: buNo,
          showAll: showAll,
          processCodes: validCodes,
          processOrderCodes: validOrderCodes
        })
      }
      const inData = {
        site: this.$store.state.user.site,
        updateBy: this.$store.state.user.name,
        rows: submitRows
      }
      saveProofTrackingBuProcessConfig(inData).then(({ data }) => {
        if (!(data && data.code === 0)) {
          this.$message.error((data && data.msg) || '事业部进度展示设置保存失败')
          return
        }
        this.buProcessConfigMap = nextMap
        this.ensureBuProcessConfigRows()
        this.buProcessConfigDialogVisible = false
        this.$message.success(data.msg || '事业部进度展示设置已保存')
        // 保存后重新拉取工序列和列表，确保页面立即按最新可见性与顺序刷新。
        this.queryProcessColumns().then(() => {
          this.getDataList()
          this.refreshTrackingTableLayout()
        })
      }).catch(() => {
        this.$message.error('事业部进度展示设置保存异常')
      })
    },
    getRoleApi (apiKey) {
      const apiMap = {
        0: searchBusinessInfo,
        1: searchBusinessInfo1,
        2: searchBusinessInfo2,
        3: searchBusinessInfo3,
        4: searchBusinessInfo4,
        5: searchBusinessInfo5,
        6: searchBusinessInfo6,
        7: searchBusinessInfo7,
        8: searchBusinessInfo8
      }
      return apiMap[apiKey]
    },
    fetchRoleListByApiKey (apiKey, queryData) {
      const apiFn = this.getRoleApi(apiKey)
      if (!apiFn) {
        return Promise.resolve([])
      }
      const params = {
        site: this.$store.state.user.site,
        username: queryData && queryData.username ? queryData.username : '',
        userDisplay: queryData && queryData.userDisplay ? queryData.userDisplay : '',
        roleDesc: '',
        active: queryData && queryData.active ? queryData.active : '',
        page: 1,
        limit: 200
      }
      return apiFn(params).then(({ data }) => {
        if (data && data.code === 0) {
          return (data.rows || []).map(item => {
            return {
              username: item.username,
              userDisplay: item.userDisplay || item.user_display || '',
              active: item.active
            }
          })
        }
        return []
      })
    },
    newCustomer () {
      this.newCustomerData.customerDesc = ''
      this.newCustomerFlag = true
    },
    getBaseList (val) {
      this.tagNo = val
      this.$nextTick(() => {
        const strVal = val === 509 ? (this.oneKeyForm.customerNo || '') : ''
        this.$refs.baseList.init(val, strVal, '')
      })
    },
    getBaseData (val) {
      if (this.tagNo === 509) {
        this.oneKeyForm.customerNo = val.customer_no
        this.oneKeyForm.customerDesc = val.customer_desc
      }
    },
    customerNoBlur () {
      if (!this.oneKeyForm.customerNo) {
        this.oneKeyForm.customerDesc = ''
        return
      }
      const params = {
        customerNo: this.oneKeyForm.customerNo,
        createBy: this.$store.state.user.name
      }
      queryCustomerList(params).then(({ data }) => {
        if (data && data.code === 0 && data.rows && data.rows.length === 1) {
          this.oneKeyForm.customerDesc = data.rows[0].customerDesc
        } else {
          this.oneKeyForm.customerDesc = ''
        }
      })
    },
    saveNewCustomer () {
      if (!this.newCustomerData.customerDesc) {
        this.$message.warning('请输入客户名称！')
        return
      }
      this.generateNextCustomerNo().then(customerNo => {
        if (!customerNo) {
          this.$alert('该客户已存在，请重新输入客户名称!', '提示', {
            confirmButtonText: '确定',
            type: 'warning'
          })
          return
        }
        const params = {
          site: this.$store.state.user.site,
          customerNo: customerNo,
          customerDesc: this.newCustomerData.customerDesc,
          createBy: this.$store.state.user.name
        }
        saveNewCustomer(params).then(({ data }) => {
          if (data && data.code === 0) {
            this.oneKeyForm.customerNo = customerNo
            this.oneKeyForm.customerDesc = this.newCustomerData.customerDesc
            this.newCustomerFlag = false
            this.$message.success(data.msg || '新增客户成功')
          } else {
            this.$alert(data && data.msg ? data.msg : '新增客户失败', '提示', {
              confirmButtonText: '确定',
              type: 'warning'
            })
          }
        })
      })
    },
    generateNextCustomerNo () {
      const params = {
        site: this.$store.state.user.site,
        customerDesc: this.newCustomerData.customerDesc,
        createBy: this.$store.state.user.name
      }
      return new Promise(resolve => {
        getCustomerNo(params).then(({ data }) => {
          if (!(data && data.code === 0)) {
            resolve(false)
            return
          }
          if (!data.data || !data.data.customerNo || data.data === '0') {
            resolve('C0001')
            return
          }
          const lastCustomerNo = parseInt(String(data.data.customerNo).substring(1), 10) || 0
          const nextCustomerNo = lastCustomerNo + 1
          if (nextCustomerNo < 10) {
            resolve('C000' + nextCustomerNo)
          } else if (nextCustomerNo < 100) {
            resolve('C00' + nextCustomerNo)
          } else if (nextCustomerNo < 1000) {
            resolve('C0' + nextCustomerNo)
          } else {
            resolve('C' + nextCustomerNo)
          }
        }).catch(() => {
          resolve(false)
        })
      })
    },
    closeNewCustomer () {
      this.newCustomerData.customerDesc = ''
      this.newCustomerFlag = false
    },
    openRoleDialog (field) {
      const roleCfg = this.roleConfig[field]
      if (!roleCfg) {
        return
      }
      this.roleDialogField = field
      this.roleDialogApiKey = roleCfg.apiKey
      this.roleDialogTitle = roleCfg.title
      this.roleSearchData = {
        site: this.$store.state.user.site,
        username: '',
        userDisplay: '',
        active: '',
        page: 1,
        limit: 50
      }
      this.roleDialogVisible = true
      this.queryRoleDialogList()
    },
    queryRoleDialogList () {
      this.fetchRoleListByApiKey(this.roleDialogApiKey, this.roleSearchData).then(rows => {
        this.roleDialogList = rows
      })
    },
    pickRole (row) {
      const roleCfg = this.roleConfig[this.roleDialogField]
      if (!roleCfg) {
        return
      }
      const value = row.username + '-' + row.userDisplay
      this.oneKeyForm[this.roleDialogField] = value
      this.oneKeyForm[roleCfg.nameField] = row.userDisplay
      this.roleDialogVisible = false
    },
    getEmptyOneKeyForm () {
      return {
        site: this.$store.state.user.site,
        buNo: '',
        projectNo: '',
        projectName: '',
        projectDesc: '',
        projectStatus: '草稿',
        projectSource: '',
        testPartNo: '',
        partDesc: '',
        partName: '',
        partSpec: '',
        materialNumber: '',
        finalPartDesc: '',
        finalPartNo: '',
        baseNo: '',
        revNo: '',
        customerNo: '',
        customerDesc: '',
        finalCustomerId: '',
        customerRemark: '',
        parentProjectNo: '',
        oriProjectId: '',
        projectCategory: '',
        cProjectRegion: '',
        projectManager: '',
        projectManagerName: '',
        projectOwner: '',
        projectOwnerName: '',
        cQualityEngineer1: '',
        cQualityEngineer1Name: '',
        cQualityEngineer2: '',
        cQualityEngineer2Name: '',
        cQualityEngineer3: '',
        cQualityEngineer3Name: '',
        cQualityEngineer4: '',
        cQualityEngineer4Name: '',
        cQualityEngineer5: '',
        cQualityEngineer5Name: '',
        cQualityEngineer6: '',
        cQualityEngineer6Name: '',
        cManufactureEngineer: '',
        cManufactureEngineerName: '',
        docEngineer: '',
        docEngineerName: '',
        docEngineer2: '',
        docEngineer2Name: '',
        ipqcHardTag: '',
        ipqcHardTagName: '',
        cQualityEngineer7: '',
        cQualityEngineer7Name: '',
        partType: '',
        partStatus: '草稿',
        projectPhase: '',
        tracker: '',
        engineer: '',
        engineerName: '',
        priorityLevel: '',
        proofingNo: '',
        proofingStatus: '草稿',
        proofingNumber: '',
        projectCreationDate: '',
        projectCloseDate: '',
        buildDate: '',
        closeDate: '',
        comments: '',
        planStartDate: '',
        requiredDeliveryDate: '',
        needDate: '',
        remark: ''
      }
    },
    getDefaultProofDialogData () {
      return {
        trackingId: null,
        site: this.$store.state.user.site,
        projectId: null,
        projectNo: '',
        projectDesc: '',
        buNo: '',
        customerNo: '',
        customerDesc: '',
        projectPartId: null,
        testPartNo: '',
        partDesc: '',
        projectCategory: '',
        cProjectTypeDb: '',
        projectPhase: '',
        projectManager: '',
        projectOwner: '',
        engineer: '',
        priorityLevel: '',
        proofingNo: '',
        proofingNumber: '',
        planStartDate: '',
        requiredDeliveryDate: '',
        actualityDeliveryDate: '',
        proofingStatus: '草稿',
        remark: '',
        createBy: this.$store.state.user.name,
        updateBy: this.$store.state.user.name
      }
    },
    getDataList (flag) {
      if (flag === 'Y') {
        this.pageIndex = 1
      }
      this.searchData.site = this.$store.state.user.site
      this.searchData.userName = this.$store.state.user.name
      const inData = Object.assign({}, this.searchData, {
        page: this.pageIndex,
        limit: this.pageSize
      })
      inData.buNo = this.normalizeBuNo(inData.buNo, inData.site)
      this.dataListLoading = true
      searchProofTracking(inData).then(({ data }) => {
        this.dataListLoading = false
        this.resetProcessTooltip()
        if (data && data.code === 0) {
          this.dataList = (data.page && data.page.list) || []
          this.syncDataListProcessValues()
          this.totalPage = (data.page && data.page.totalCount) || 0
          if (this.commentsEditingTrackingId) {
            const hasEditingRow = this.dataList.some(item => item.trackingId === this.commentsEditingTrackingId)
            if (!hasEditingRow) {
              this.cancelCommentsEdit()
            }
          }
          const manualSelectedId = this.manualSelectedTrackingId
          const previousCurrentId = this.currentRow && this.currentRow.trackingId
          let nextCurrentRow = null
          if (manualSelectedId != null) {
            // 只要用户没有手动点新行，刷新后始终尝试回选上次手动选中的那一行
            nextCurrentRow = this.dataList.find(item => item && item.trackingId === manualSelectedId) || null
          } else if (previousCurrentId != null) {
            nextCurrentRow = this.dataList.find(item => item && item.trackingId === previousCurrentId) || null
          }
          if (!nextCurrentRow && manualSelectedId == null && this.dataList.length > 0) {
            nextCurrentRow = this.dataList[0]
          }
          this.currentRow = nextCurrentRow
          this.$nextTick(() => {
            if (this.$refs.trackingTable && typeof this.$refs.trackingTable.setCurrentRow === 'function') {
              this.$refs.trackingTable.setCurrentRow(this.currentRow || null)
            }
          })
        } else {
          this.dataList = []
          this.totalPage = 0
          this.currentRow = null
          this.$nextTick(() => {
            if (this.$refs.trackingTable && typeof this.$refs.trackingTable.setCurrentRow === 'function') {
              this.$refs.trackingTable.setCurrentRow(null)
            }
          })
          this.$message.error(data.msg || '查询失败')
        }
      }).catch(() => {
        this.dataListLoading = false
        this.resetProcessTooltip()
        this.$message.error('查询异常')
      })
    },
    resetSearch () {
      this.searchData.projectNo = ''
      this.searchData.projectDesc = ''
      this.searchData.buNo = ''
      this.searchData.testPartNo = ''
      this.searchData.partDesc = ''
      this.searchData.customerNo = ''
      this.searchData.proofingNo = ''
      this.searchData.projectPartSyncFlag = ''
      this.searchData.proofSyncFlag = ''
      this.searchData.proofingStatus = ''
      this.queryProcessColumns().then(() => {
        this.getDataList('Y')
      })
    },
    selectionChange (rows) {
      this.selectionRows = rows || []
    },
    isCommentsEditing (row) {
      return !!row && row.trackingId === this.commentsEditingTrackingId
    },
    startCommentsEdit (row) {
      if (this.batchEditMode || !row || !row.trackingId || this.commentsSaveLoading) {
        return
      }
      this.commentsEditingTrackingId = row.trackingId
      this.commentsDraft = row.comments || ''
    },
    cancelCommentsEdit () {
      if (this.commentsSaveLoading) {
        return
      }
      this.commentsEditingTrackingId = null
      this.commentsDraft = ''
    },
    handleCommentsBlur (row) {
      if (this.commentsSaveLoading || !this.isCommentsEditing(row)) {
        return
      }
      const comments = this.commentsDraft == null ? '' : String(this.commentsDraft)
      if (comments.trim()) {
        this.saveCommentsEdit(row, {
          fromBlur: true,
          draftComments: comments
        })
        return
      }
      this.cancelCommentsEdit()
    },
    saveCommentsEdit (row, options) {
      const saveOptions = options || {}
      const fromBlur = !!saveOptions.fromBlur
      if (this.commentsSaveLoading) {
        return
      }
      if (!row || !row.trackingId) {
        this.$message.warning('跟踪记录ID不能为空')
        return
      }
      const comments = Object.prototype.hasOwnProperty.call(saveOptions, 'draftComments')
        ? (saveOptions.draftComments == null ? '' : String(saveOptions.draftComments))
        : (this.commentsDraft == null ? '' : String(this.commentsDraft))
      if (comments === (row.comments || '')) {
        this.cancelCommentsEdit()
        return
      }
      if (fromBlur) {
        this.commentsEditingTrackingId = null
        this.commentsDraft = ''
      }
      this.commentsSaveLoading = true
      updateProofTrackingComments({
        trackingId: row.trackingId,
        comments: comments,
        updateBy: this.$store.state.user.name
      }).then(({ data }) => {
        this.commentsSaveLoading = false
        if (data && data.code === 0) {
          row.comments = comments
          this.$message.success('Comments保存成功')
          this.cancelCommentsEdit()
        } else {
          this.$message.error(data.msg || 'Comments保存失败')
        }
      }).catch(() => {
        this.commentsSaveLoading = false
        this.$message.error('Comments保存异常')
      })
    },
    rowClick (row) {
      this.currentRow = row
      this.manualSelectedTrackingId = row && row.trackingId ? row.trackingId : null
    },
    sizeChangeHandle (val) {
      this.pageSize = val
      this.pageIndex = 1
      this.getDataList()
    },
    currentChangeHandle (val) {
      this.pageIndex = val
      this.getDataList()
    },
    getActionRow (row) {
      return row || this.currentRow || (this.selectionRows.length > 0 ? this.selectionRows[0] : null)
    },
    getTrackingProjectPartId (row) {
      if (!row) {
        return null
      }
      return row.projectPartId || row.project_part_id || null
    },
    isSyncedByIdRule (idValue) {
      if (idValue == null || String(idValue).trim() === '') {
        return false
      }
      // 约定：NPI 正式库主键 < 100000，tracking 临时主键 >= 100000。
      const numericId = Number(idValue)
      return Number.isFinite(numericId) && numericId > 0 && numericId < 100000
    },
    isProjectPartSynced (row) {
      if (!row) {
        return false
      }
      const projectId = row.projectId || row.project_id || null
      const projectPartId = this.getTrackingProjectPartId(row)
      return this.isSyncedByIdRule(projectId) && this.isSyncedByIdRule(projectPartId)
    },
    isProofSynced (row) {
      if (!row) {
        return false
      }
      const proofingId = row.proofingId || row.proofing_id || row.trackingId || null
      return this.isSyncedByIdRule(proofingId)
    },
    getProjectPartSyncTextClass (row) {
      return this.isProjectPartSynced(row) ? 'sync-text--ok' : 'sync-text--pending'
    },
    getProofSyncTextClass (row) {
      return this.isProofSynced(row) ? 'sync-text--ok' : 'sync-text--pending'
    },
    getProjectCategoryValue (source) {
      if (!source) {
        return ''
      }
      const category = source.cProjectTypeDb || source.projectCategory || source.project_category || source.c_project_type_db || ''
      return this.isBlankValue(category) ? '' : String(category).trim()
    },
    isOneKeyCreateFieldVisible (fieldKey) {
      if (this.oneKeyDialogMode !== 'create') {
        return true
      }
      return this.oneKeyCreateVisibleFieldKeys.indexOf(fieldKey) > -1
    },
    isBlankValue (value) {
      return value == null || String(value).trim() === ''
    },
    normalizeProjectNo (projectNo) {
      if (this.isBlankValue(projectNo)) {
        return ''
      }
      return String(projectNo).trim()
    },
    queryApplyOptionsByProjectNo (projectNo) {
      const normalizedProjectNo = this.normalizeProjectNo(projectNo)
      if (!normalizedProjectNo) {
        return Promise.resolve([])
      }
      return searchExpApplyList({
        projectNo: normalizedProjectNo,
        page: 1,
        limit: 500
      }).then(({ data }) => {
        if (!(data && data.code === 0)) {
          throw new Error((data && data.msg) || '查询试验单失败')
        }
        const pageList = (data.page && data.page.list) || []
        const matchedProjectNo = normalizedProjectNo.toUpperCase()
        const optionMap = {}
        const optionRows = []
        pageList.forEach(item => {
          if (!item || this.isBlankValue(item.applyNo)) {
            return
          }
          const applyStatus = this.isBlankValue(item.status) ? '' : String(item.status).trim()
          // 业务要求：已完成的试验单不允许继续作为 tracking 打样单号使用。
          if (applyStatus === '草稿' || applyStatus === '已完成') {
            return
          }
          const rowProjectNo = this.normalizeProjectNo(item.projectNo).toUpperCase()
          // searchExpApplyList 按模糊 project_no 查询，这里再次做等值过滤，确保与 tracking 项目编码一一对应。
          if (rowProjectNo !== matchedProjectNo) {
            return
          }
          const applyNo = String(item.applyNo).trim()
          if (optionMap[applyNo]) {
            return
          }
          optionMap[applyNo] = true
          optionRows.push({
            applyNo: applyNo,
            expectedFinishDate: this.formatDate(item.expectedFinishDate)
          })
        })
        return optionRows
      })
    },
    handleOneKeyProjectNoBlur () {
      const projectNo = this.normalizeProjectNo(this.oneKeyForm.projectNo)
      if (!projectNo) {
        this.oneKeyProofingApplyQueryToken += 1
        this.oneKeyProofingApplyLoading = false
        this.oneKeyProofingApplyOptions = []
        this.oneKeyForm.proofingNo = ''
        this.oneKeyForm.requiredDeliveryDate = ''
        return
      }
      this.loadOneKeyProofingApplyOptions(projectNo, { keepCurrentIfMissing: false })
    },
    handleOneKeyProofingNoChange (proofingNo) {
      const selectedNo = this.isBlankValue(proofingNo) ? '' : String(proofingNo).trim()
      if (!selectedNo) {
        this.oneKeyForm.requiredDeliveryDate = ''
        return
      }
      const matched = this.oneKeyProofingApplyOptions.find(item => item && item.applyNo === selectedNo)
      if (!matched) {
        return
      }
      this.oneKeyForm.proofingNo = matched.applyNo
      this.oneKeyForm.requiredDeliveryDate = matched.expectedFinishDate || ''
    },
    loadOneKeyProofingApplyOptions (projectNo, options) {
      const queryOptions = options || {}
      const normalizedProjectNo = this.normalizeProjectNo(projectNo)
      const currentProofingNo = this.isBlankValue(this.oneKeyForm.proofingNo)
        ? ''
        : String(this.oneKeyForm.proofingNo).trim()
      const queryToken = this.oneKeyProofingApplyQueryToken + 1
      this.oneKeyProofingApplyQueryToken = queryToken
      if (!normalizedProjectNo) {
        this.oneKeyProofingApplyLoading = false
        this.oneKeyProofingApplyOptions = []
        return Promise.resolve([])
      }
      this.oneKeyProofingApplyLoading = true
      return this.queryApplyOptionsByProjectNo(normalizedProjectNo).then(optionRows => {
        if (queryToken !== this.oneKeyProofingApplyQueryToken) {
          return optionRows
        }
        const nextOptions = optionRows.slice()
        const keepCurrentIfMissing = !!queryOptions.keepCurrentIfMissing
        if (currentProofingNo) {
          const hasCurrent = nextOptions.some(item => item && item.applyNo === currentProofingNo)
          if (!hasCurrent && keepCurrentIfMissing) {
            // 历史记录可能早于 ERF 规范化，保留原打样单号避免编辑场景被强制清空。
            nextOptions.unshift({
              applyNo: currentProofingNo,
              expectedFinishDate: this.formatDate(this.oneKeyForm.requiredDeliveryDate)
            })
          }
          if (!hasCurrent && !keepCurrentIfMissing) {
            this.oneKeyForm.proofingNo = ''
            this.oneKeyForm.requiredDeliveryDate = ''
          }
        }
        this.oneKeyProofingApplyOptions = nextOptions
        if (currentProofingNo) {
          this.handleOneKeyProofingNoChange(currentProofingNo)
        }
        return nextOptions
      }).catch((e) => {
        if (queryToken === this.oneKeyProofingApplyQueryToken) {
          this.oneKeyProofingApplyOptions = []
          this.$message.error((e && e.message) || '查询试验单异常')
        }
        return []
      }).finally(() => {
        if (queryToken === this.oneKeyProofingApplyQueryToken) {
          this.oneKeyProofingApplyLoading = false
        }
      })
    },
    handleProofDialogProofingNoChange (proofingNo) {
      const selectedNo = this.isBlankValue(proofingNo) ? '' : String(proofingNo).trim()
      if (!selectedNo) {
        this.proofDialogData.requiredDeliveryDate = ''
        return
      }
      const matched = this.proofDialogApplyOptions.find(item => item && item.applyNo === selectedNo)
      if (!matched) {
        return
      }
      this.proofDialogData.proofingNo = matched.applyNo
      this.proofDialogData.requiredDeliveryDate = matched.expectedFinishDate || ''
    },
    loadProofDialogApplyOptions (projectNo) {
      const normalizedProjectNo = this.normalizeProjectNo(projectNo)
      const queryToken = this.proofDialogApplyQueryToken + 1
      this.proofDialogApplyQueryToken = queryToken
      if (!normalizedProjectNo) {
        this.proofDialogApplyLoading = false
        this.proofDialogApplyOptions = []
        this.proofDialogData.proofingNo = ''
        this.proofDialogData.requiredDeliveryDate = ''
        return Promise.resolve([])
      }
      this.proofDialogApplyLoading = true
      return this.queryApplyOptionsByProjectNo(normalizedProjectNo).then(optionRows => {
        if (queryToken !== this.proofDialogApplyQueryToken) {
          return optionRows
        }
        this.proofDialogApplyOptions = optionRows
        const currentProofingNo = this.isBlankValue(this.proofDialogData.proofingNo)
          ? ''
          : String(this.proofDialogData.proofingNo).trim()
        if (currentProofingNo) {
          this.handleProofDialogProofingNoChange(currentProofingNo)
        }
        return optionRows
      }).catch((e) => {
        if (queryToken === this.proofDialogApplyQueryToken) {
          this.proofDialogApplyOptions = []
          this.$message.error((e && e.message) || '查询试验单异常')
        }
        return []
      }).finally(() => {
        if (queryToken === this.proofDialogApplyQueryToken) {
          this.proofDialogApplyLoading = false
        }
      })
    },
    getBuOptionValue (site, buNo) {
      const rawBuNo = buNo == null ? '' : String(buNo).trim()
      if (!rawBuNo) {
        return ''
      }
      if (rawBuNo.indexOf('_') > -1) {
        return rawBuNo
      }
      const targetSuffix = `_${rawBuNo}`
      const matched = this.userBuList.find(item => item && item.buNo && (item.buNo === rawBuNo || item.buNo === `${site}_${rawBuNo}` || String(item.buNo).endsWith(targetSuffix)))
      if (matched && matched.buNo) {
        return matched.buNo
      }
      return site ? `${site}_${rawBuNo}` : rawBuNo
    },
    getRoleDisplayName (value) {
      if (this.isBlankValue(value)) {
        return ''
      }
      const strVal = String(value)
      const splitIdx = strVal.indexOf('-')
      return splitIdx > -1 ? strVal.substring(splitIdx + 1) : strVal
    },
    buildOneKeySubmitPayload () {
      const inData = Object.assign({}, this.oneKeyForm, {
        createBy: this.$store.state.user.name,
        updateBy: this.$store.state.user.name
      })
      if (!inData.projectName && inData.projectDesc) {
        inData.projectName = inData.projectDesc
      }
      if (!inData.projectDesc && inData.projectName) {
        inData.projectDesc = inData.projectName
      }
      if (!inData.tracker) {
        inData.tracker = inData.projectOwner
      }
      if (!inData.priorityLevel) {
        inData.priorityLevel = ''
      }
      // 弹窗仅展示一个立项日期输入，提交时同步到项目与物料两个日期字段，避免物料立项日期被误判为空
      if (!this.isBlankValue(inData.projectCreationDate)) {
        inData.buildDate = inData.projectCreationDate
      } else if (!this.isBlankValue(inData.buildDate)) {
        inData.projectCreationDate = inData.buildDate
      }
      return inData
    },
    validateOneKeySubmitPayload (inData) {
      if (this.isBlankValue(inData.site) || this.isBlankValue(inData.buNo)) {
        this.$message.error('工厂和BU不能为空')
        return false
      }
      // create 模式在保存前要求补齐关键责任人与料号描述，避免创建后出现主责缺失数据。
      if (this.oneKeyDialogMode === 'create') {
        if (this.isOneKeyCreateFieldVisible('partDesc') && this.isBlankValue(inData.partDesc)) {
          this.$message.error('料号描述不能为空')
          return false
        }
        if (this.isOneKeyCreateFieldVisible('projectOwner') && this.isBlankValue(inData.projectOwner)) {
          this.$message.error('PjM不能为空')
          return false
        }
        if (this.isOneKeyCreateFieldVisible('engineer') && this.isBlankValue(inData.engineer)) {
          this.$message.error('Engineer不能为空')
          return false
        }
      }
      if (this.oneKeyDialogMode === 'edit') {
        if (!inData.projectId || !inData.projectPartId || !inData.trackingId) {
          this.$message.error('项目/物料/打样记录ID缺失，无法修改')
          return false
        }
        // 修改模式按业务要求仅保留 3 个必填项，其他业务字段允许为空。
        if (this.isBlankValue(inData.partDesc)) {
          this.$message.error('料号描述不能为空')
          return false
        }
        if (this.isBlankValue(inData.projectOwner)) {
          this.$message.error('PjM不能为空')
          return false
        }
        if (this.isBlankValue(inData.engineer)) {
          this.$message.error('Engineer不能为空')
          return false
        }
        if (!this.isBlankValue(inData.proofingNumber) && !/^[1-9]\d*$/.test(String(inData.proofingNumber))) {
          this.$message.error('打样数量必须是正整数')
          return false
        }
      }
      return true
    },
    buildOneKeyEditForm (current, projectData, partData) {
      const project = projectData || {}
      const part = partData || {}
      const form = Object.assign(this.getEmptyOneKeyForm(), {
        site: project.site || part.site || current.site || this.$store.state.user.site,
        projectId: project.projectId || current.projectId || null,
        projectPartId: part.projectPartId || this.getTrackingProjectPartId(current),
        trackingId: current.trackingId || null,
        buNo: this.getBuOptionValue(project.site || current.site || this.$store.state.user.site, project.buNo || current.buNo),
        projectNo: project.projectNo || current.projectNo || '',
        projectName: project.projectName || project.projectDesc || current.projectName || current.projectDesc || '',
        projectDesc: project.projectDesc || project.projectName || current.projectDesc || current.projectName || '',
        projectStatus: project.status || project.projectStatus || '草稿',
        projectSource: project.projectSource || '',
        testPartNo: part.testPartNo || current.testPartNo || '',
        partDesc: part.partDesc || current.partDesc || '',
        partName: part.partName || '',
        partSpec: part.partSpec || '',
        materialNumber: part.materialNumber || '',
        finalPartDesc: part.finalPartDesc || '',
        finalPartNo: part.finalPartNo || current.finalPartNo || '',
        baseNo: part.baseNo || '',
        revNo: part.revNo || '',
        customerNo: project.customerNo || part.customerNo || current.customerNo || '',
        customerDesc: project.customerDesc || current.customerDesc || '',
        finalCustomerId: project.finalCustomerId || '',
        customerRemark: project.customerRemark || '',
        parentProjectNo: project.parentProjectNo || '',
        oriProjectId: project.oriProjectId || '',
        projectCategory: project.projectCategory || part.projectCategory || current.projectCategory || '',
        cProjectRegion: project.cProjectRegion || current.cProjectRegion || '',
        projectManager: project.projectManager || part.projectManager || current.projectManager || '',
        projectOwner: project.projectOwner || part.projectOwner || current.projectOwner || '',
        cQualityEngineer1: project.cQualityEngineer1 || part.cQualityEngineer1 || '',
        cQualityEngineer2: project.cQualityEngineer2 || part.cQualityEngineer2 || '',
        cQualityEngineer3: project.cQualityEngineer3 || part.cQualityEngineer3 || '',
        cQualityEngineer4: project.cQualityEngineer4 || part.cQualityEngineer4 || '',
        cQualityEngineer5: project.cQualityEngineer5 || part.cQualityEngineer5 || '',
        cQualityEngineer6: project.cQualityEngineer6 || part.cQualityEngineer6 || '',
        cManufactureEngineer: project.cManufactureEngineer || part.cManufactureEngineer || '',
        docEngineer: project.docEngineer || part.docEngineer || '',
        docEngineer2: project.docEngineer2 || part.docEngineer2 || '',
        ipqcHardTag: project.ipqcHardTag || part.ipqcHardTag || '',
        cQualityEngineer7: project.cQualityEngineer7 || part.cQualityEngineer7 || '',
        partType: part.partType || '',
        partStatus: part.status || part.partStatus || '草稿',
        projectPhase: current.projectPhase || '',
        tracker: current.tracker || project.projectOwner || part.projectOwner || '',
        engineer: current.engineer || project.engineer || part.engineer || '',
        priorityLevel: current.priorityLevel || current.priority || project.priority || part.priority || '',
        proofingNo: current.proofingNo || '',
        proofingStatus: current.proofingStatus || '进行中',
        proofingNumber: current.proofingNumber,
        projectCreationDate: project.projectCreationDate || part.buildDate || '',
        projectCloseDate: project.projectCloseDate || '',
        buildDate: part.buildDate || project.projectCreationDate || '',
        closeDate: part.closeDate || '',
        comments: current.comments || '',
        planStartDate: current.planStartDate || '',
        requiredDeliveryDate: current.requiredDeliveryDate || '',
        needDate: project.needDate || part.needDate || current.needDate || '',
        remark: part.remark || project.remark || current.remark || ''
      })
      form.projectManagerName = this.getRoleDisplayName(form.projectManager)
      form.projectOwnerName = this.getRoleDisplayName(form.projectOwner)
      form.engineerName = this.getRoleDisplayName(form.engineer)
      form.cManufactureEngineerName = this.getRoleDisplayName(form.cManufactureEngineer)
      form.cQualityEngineer1Name = this.getRoleDisplayName(form.cQualityEngineer1)
      form.cQualityEngineer2Name = this.getRoleDisplayName(form.cQualityEngineer2)
      form.cQualityEngineer3Name = this.getRoleDisplayName(form.cQualityEngineer3)
      form.cQualityEngineer4Name = this.getRoleDisplayName(form.cQualityEngineer4)
      form.cQualityEngineer5Name = this.getRoleDisplayName(form.cQualityEngineer5)
      form.cQualityEngineer6Name = this.getRoleDisplayName(form.cQualityEngineer6)
      form.docEngineerName = this.getRoleDisplayName(form.docEngineer)
      form.docEngineer2Name = this.getRoleDisplayName(form.docEngineer2)
      form.ipqcHardTagName = this.getRoleDisplayName(form.ipqcHardTag)
      form.cQualityEngineer7Name = this.getRoleDisplayName(form.cQualityEngineer7)
      return form
    },
    openOneKeyDialog (mode, row) {
      const dialogMode = mode === 'edit' ? 'edit' : 'create'
      this.oneKeyDialogMode = dialogMode
      if (dialogMode !== 'edit') {
        this.oneKeyProofingApplyQueryToken += 1
        this.oneKeyProofingApplyLoading = false
        this.oneKeyProofingApplyOptions = []
        this.oneKeyForm = this.getEmptyOneKeyForm()
        if (this.userBuList.length > 0) {
          this.oneKeyForm.buNo = this.userBuList[0].buNo || ''
        }
        this.oneKeyDialogVisible = true
        return
      }
      const current = this.getActionRow(row)
      if (!current) {
        this.$message.warning('请先选择一条记录后再修改项目/物料/打样')
        this.oneKeyDialogMode = 'create'
        return
      }
      const projectPartId = this.getTrackingProjectPartId(current)
      if (!current.projectId || !projectPartId || !current.trackingId) {
        this.$message.warning('当前记录缺少项目/物料/打样关键信息，无法修改')
        this.oneKeyDialogMode = 'create'
        return
      }
      const queryUser = this.$store.state.user.name
      const querySite = current.site || this.$store.state.user.site
      this.saveOneKeyLoading = true
      Promise.all([
        searchProjectInfoTracking({
          site: querySite,
          userName: queryUser,
          projectId: current.projectId,
          page: 1,
          limit: 1
        }),
        searchProjectPartTracking({
          site: querySite,
          userName: queryUser,
          projectPartId: projectPartId,
          page: 1,
          limit: 1
        })
      ]).then(([projectResp, partResp]) => {
        const projectData = projectResp && projectResp.data
        const partData = partResp && partResp.data
        if (!projectData || projectData.code !== 0) {
          throw new Error((projectData && projectData.msg) || '加载项目信息失败')
        }
        if (!partData || partData.code !== 0) {
          throw new Error((partData && partData.msg) || '加载项目物料信息失败')
        }
        const projectRows = (projectData.page && projectData.page.list) || []
        const partRows = (partData.page && partData.page.list) || []
        if (projectRows.length === 0) {
          throw new Error('未找到对应的项目信息，无法修改')
        }
        if (partRows.length === 0) {
          throw new Error('未找到对应的项目物料信息，无法修改')
        }
        this.oneKeyForm = this.buildOneKeyEditForm(current, projectRows[0], partRows[0])
        this.oneKeyProofingApplyQueryToken += 1
        this.oneKeyProofingApplyLoading = false
        this.oneKeyProofingApplyOptions = []
        this.oneKeyDialogVisible = true
        this.loadOneKeyProofingApplyOptions(this.oneKeyForm.projectNo, { keepCurrentIfMissing: true })
      }).catch((e) => {
        this.$message.error((e && e.message) || '加载项目/物料/打样信息异常')
        this.oneKeyDialogMode = 'create'
      }).finally(() => {
        this.saveOneKeyLoading = false
      })
    },
    submitOneKey () {
      const inData = this.buildOneKeySubmitPayload()
      if (!this.validateOneKeySubmitPayload(inData)) {
        return
      }
      this.saveOneKeyLoading = true
      if (this.oneKeyDialogMode === 'edit') {
        oneKeyUpdateProofTracking(inData).then(({ data }) => {
          if (!data || data.code !== 0) {
            throw new Error((data && data.msg) || '修改项目/物料/打样失败')
          }
          this.$message.success('项目/物料/打样修改成功')
          this.oneKeyDialogVisible = false
          this.getDataList()
        }).catch((e) => {
          this.$message.error((e && e.message) || '项目/物料/打样修改异常')
        }).finally(() => {
          this.saveOneKeyLoading = false
        })
        return
      }
      oneKeyCreateProofTracking(inData).then(({ data }) => {
        this.saveOneKeyLoading = false
        if (data && data.code === 0) {
          this.$message.success('一键创建成功')
          this.oneKeyDialogVisible = false
          this.searchData.projectNo = ''
          this.searchData.projectDesc = ''
          this.searchData.testPartNo = ''
          this.searchData.partDesc = ''
          this.searchData.customerNo = ''
          this.searchData.proofingNo = ''
          this.searchData.projectPartSyncFlag = ''
          this.searchData.proofSyncFlag = ''
          this.searchData.proofingStatus = ''
          this.getDataList('Y')
        } else {
          this.$message.error(data.msg || '一键创建失败')
        }
      }).catch(() => {
        this.saveOneKeyLoading = false
        this.$message.error('一键创建异常')
      })
    },
    openCreateProofDialog (row) {
      const current = this.getActionRow(row)
      if (!current) {
        this.$message.warning('请先选择一条记录')
        return
      }
      this.currentRow = current
      const projectPartId = this.getTrackingProjectPartId(current) || current.id
      const projectCategory = this.getProjectCategoryValue(current)
      this.proofDialogApplyQueryToken += 1
      this.proofDialogApplyLoading = false
      this.proofDialogApplyOptions = []
      this.proofDialogData = Object.assign(this.getDefaultProofDialogData(), {
        trackingId: null,
        site: current.site || this.$store.state.user.site,
        projectId: current.projectId,
        projectNo: current.projectNo,
        projectDesc: current.projectDesc,
        buNo: current.buNo,
        customerNo: current.customerNo,
        customerDesc: current.customerDesc,
        projectPartId: projectPartId,
        testPartNo: current.testPartNo,
        partDesc: current.partDesc,
        projectCategory: projectCategory,
        cProjectTypeDb: projectCategory,
        projectManager: current.projectManager,
        projectOwner: current.projectOwner,
        engineer: current.engineer,
        priorityLevel: current.priority,
        proofingNo: '',
        proofingNumber: '',
        planStartDate: '',
        requiredDeliveryDate: '',
        actualityDeliveryDate: '',
        proofingStatus: '草稿',
        remark: '',
        createBy: this.$store.state.user.name,
        updateBy: this.$store.state.user.name
      })
      if (this.isBlankValue(projectCategory)) {
        this.$message.warning('未获取到项目分类，请手动选择')
      }
      this.proofDialogVisible = true
      this.loadProofDialogApplyOptions(this.proofDialogData.projectNo)
    },
    validateSyncToNpiRow (row) {
      const projectPartId = this.getTrackingProjectPartId(row)
      const missingLabels = []
      if (this.isBlankValue(row && row.site)) {
        missingLabels.push('工厂')
      }
      if (this.isBlankValue(row && row.buNo)) {
        missingLabels.push('BU')
      }
      if (!row || !row.projectId) {
        missingLabels.push('项目ID')
      }
      if (this.isBlankValue(row && row.projectNo)) {
        missingLabels.push('项目编码')
      }
      if (this.isBlankValue(row && row.projectName) && this.isBlankValue(row && row.projectDesc)) {
        missingLabels.push('项目名称')
      }
      if (!projectPartId) {
        missingLabels.push('项目物料ID')
      }
      if (this.isBlankValue(row && row.testPartNo)) {
        missingLabels.push('项目料号')
      }
      if (this.isBlankValue(row && row.partDesc)) {
        missingLabels.push('料号描述')
      }
      if (missingLabels.length > 0) {
        const missingHtml = missingLabels.map(item => `- ${item}`).join('<br/>')
        this.$alert(`同步到NPI前，请先完善以下字段：<br/>${missingHtml}`, '提示', {
          confirmButtonText: '确定',
          dangerouslyUseHTMLString: true
        })
        return {
          passed: false,
          projectPartId: projectPartId
        }
      }
      return {
        passed: true,
        projectPartId: projectPartId
      }
    },
    syncToNpi (row) {
      const current = this.getActionRow(row)
      if (!current) {
        this.$message.warning('请先选择一条记录')
        return
      }
      const syncCheck = this.validateSyncToNpiRow(current)
      if (!syncCheck.passed) {
        return
      }
      const projectPartId = syncCheck.projectPartId
      this.currentRow = current
      const projectNo = current.projectNo || '-'
      const testPartNo = current.testPartNo || '-'
      const confirmMsg = `确认将项目编码[${projectNo}]物料编码[${testPartNo}]同步到NPI吗？`
      this.$confirm(confirmMsg, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
        dangerouslyUseHTMLString: true
      }).then(() => {
        const operator = this.$store.state.user.name
        this.syncNpiLoading = true
        syncProjectPartTracking({
          projectPartId: projectPartId,
          updateBy: operator,
          createBy: operator,
          userName: operator
        }).then(({ data }) => {
          if (data && data.code === 0) {
            this.$message.success(`同步成功`)
            this.getDataList()
          } else {
            const failMsg = (data && data.msg) || '同步到NPI失败'
            if (failMsg.indexOf('缺少以下必填项') > -1) {
              this.$alert(failMsg, '同步校验未通过', {
                confirmButtonText: '确定'
              })
            } else {
              this.$message.error(failMsg)
            }
          }
        }).catch(() => {
          this.$message.error('同步到NPI异常')
        }).finally(() => {
          this.syncNpiLoading = false
        })
      })
      .catch(() => {})
    },
    syncProofToNpi (row) {
      const current = this.getActionRow(row)
      if (!current || !current.trackingId) {
        this.$message.warning('请先选择一条打样记录')
        return
      }
      this.currentRow = current
      const projectNo = current.projectNo || '-'
      const testPartNo = current.testPartNo || '-'
      const proofingNo = current.proofingNo || '-'
      const confirmMsg = `确认将当前打样记录同步到NPI吗？<br/>项目编码：${projectNo}<br/>物料编码：${testPartNo}<br/>打样单号：${proofingNo}`
      this.$confirm(confirmMsg, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
        dangerouslyUseHTMLString: true
      }).then(() => {
        const operator = this.$store.state.user.name
        this.syncProofLoading = true
        syncProofTracking({
          trackingId: current.trackingId,
          updateBy: operator,
          createBy: operator,
          userName: operator
        }).then(({ data }) => {
          if (data && data.code === 0) {
            this.$message.success('打样同步成功')
            this.getDataList()
          } else {
            const failMsg = (data && data.msg) || '同步打样到NPI失败'
            if (failMsg.indexOf('缺少以下必填项') > -1 || failMsg.indexOf('请先同步项目') > -1 || failMsg.indexOf('请先同步项目物料') > -1) {
              this.$alert(failMsg, '同步校验未通过', {
                confirmButtonText: '确定'
              })
            } else {
              this.$message.error(failMsg)
            }
          }
        }).catch(() => {
          this.$message.error('同步打样到NPI异常')
        }).finally(() => {
          this.syncProofLoading = false
        })
      }).catch(() => {})
    },
    validateProofDialogData () {
      if (!this.proofDialogData.projectId || !this.proofDialogData.projectPartId) {
        this.$message.warning('项目或项目料号信息不完整')
        return false
      }
      if (!this.proofDialogData.cProjectTypeDb) {
        this.$message.warning('项目分类不能为空')
        return false
      }
      if (!this.proofDialogData.projectPhase) {
        this.$message.warning('项目阶段不能为空')
        return false
      }
      if (!this.proofDialogData.proofingNo) {
        this.$message.warning('打样单号不能为空')
        return false
      }
      if (!this.proofDialogData.proofingNumber) {
        this.$message.warning('数量不能为空')
        return false
      }
      if (!/^[1-9]\d*$/.test(String(this.proofDialogData.proofingNumber))) {
        this.$alert('数量必需是大于等于0的正整数', '提示', {
          confirmButtonText: '确定'
        })
        return false
      }
      if (!this.proofDialogData.planStartDate) {
        this.$message.warning('打样开始日期不能为空')
        return false
      }
      if (!this.proofDialogData.requiredDeliveryDate) {
        this.$message.warning('预计完成日期不能为空')
        return false
      }
      return true
    },
    buildProofPayload () {
      return {
        site: this.proofDialogData.site,
        projectId: this.proofDialogData.projectId,
        projectPartId: this.proofDialogData.projectPartId,
        projectNo: this.proofDialogData.projectNo,
        testPartNo: this.proofDialogData.testPartNo,
        customerNo: this.proofDialogData.customerNo,
        buNo: this.proofDialogData.buNo,
        projectCategory: this.proofDialogData.cProjectTypeDb,
        projectPhase: this.proofDialogData.projectPhase,
        proofingNo: this.proofDialogData.proofingNo,
        proofingNumber: Number(this.proofDialogData.proofingNumber),
        planStartDate: this.proofDialogData.planStartDate,
        requiredDeliveryDate: this.proofDialogData.requiredDeliveryDate,
        actualityDeliveryDate: this.proofDialogData.actualityDeliveryDate,
        proofingStatus: this.proofDialogData.proofingStatus || '草稿',
        remark: this.proofDialogData.remark,
        tracker: this.proofDialogData.projectOwner,
        engineer: this.proofDialogData.engineer,
        priorityLevel: this.proofDialogData.priorityLevel,
        createBy: this.$store.state.user.name,
        updateBy: this.$store.state.user.name
      }
    },
    saveProofRecord () {
      if (!this.validateProofDialogData()) {
        return
      }
      const payload = this.buildProofPayload()
      this.proofSaveLoading = true
      createProofTrackingRecord(payload).then(({ data }) => {
        this.proofSaveLoading = false
        if (data && data.code === 0) {
          this.$message.success(data.msg || '新增打样成功')
          this.proofDialogVisible = false
          this.getDataList()
        } else {
          this.$alert(data && data.msg ? data.msg : '新增打样失败', '错误', { confirmButtonText: '确定' })
        }
      }).catch(() => {
        this.proofSaveLoading = false
        this.$message.error('新增打样异常')
      })
    },
    toggleBatchEdit () {
      this.batchEditMode = !this.batchEditMode
      if (this.batchEditMode) {
        this.cancelCommentsEdit()
      }
      this.$nextTick(() => {
        const tableRef = this.$refs.trackingTable
        if (tableRef && tableRef.doLayout) {
          tableRef.doLayout()
        }
        if (this.batchEditMode) {
          setTimeout(() => {
            this.scrollToRawMaterialColumn()
          }, 0)
        }
      })
    },
    scrollToRawMaterialColumn () {
      const tableRef = this.$refs.trackingTable
      if (!tableRef || !tableRef.$el) {
        return
      }
      const headerWrapper = tableRef.headerWrapper || tableRef.$el.querySelector('.el-table__header-wrapper')
      const bodyWrapper = tableRef.bodyWrapper || tableRef.$el.querySelector('.el-table__body-wrapper')
      if (!headerWrapper || !bodyWrapper) {
        return
      }
      const headerCells = headerWrapper.querySelectorAll('th')
      let targetCell = null
      for (let i = 0; i < headerCells.length; i++) {
        const text = (headerCells[i].innerText || '').replace(/\s+/g, ' ').trim()
        if (text === 'Raw Material') {
          targetCell = headerCells[i]
          break
        }
      }
      if (!targetCell) {
        return
      }
      const fixedLeftWrapper = tableRef.$el.querySelector('.el-table__fixed')
      const fixedLeftWidth = fixedLeftWrapper ? fixedLeftWrapper.offsetWidth : 0
      const nextScrollLeft = Math.max(targetCell.offsetLeft - fixedLeftWidth - 8, 0)
      bodyWrapper.scrollLeft = nextScrollLeft
      headerWrapper.scrollLeft = nextScrollLeft
    },
    saveBatchPlan () {
      const rows = this.selectionRows && this.selectionRows.length > 0 ? this.selectionRows : this.dataList
      if (!rows || rows.length === 0) {
        this.$message.warning('没有可保存的数据')
        return
      }
      const payloadRows = rows.map(row => ({
        trackingId: row.trackingId,
        rawMaterialActualDate: row.rawMaterialActualDate,
        cylinderCutterActualDate: row.cylinderCutterActualDate,
        laminationActualDate: row.laminationActualDate,
        printingActualDate: row.printingActualDate,
        etchingActualDate: row.etchingActualDate,
        slittingPackingActualDate: row.slittingPackingActualDate,
        bondingActualDate: row.bondingActualDate,
        cl60Cl822ActualDate: row.cl60Cl822ActualDate,
        spottingActualDate: row.spottingActualDate,
        encodingActualDate: row.encodingActualDate,
        dieCutActualDate: row.dieCutActualDate,
        inspectionActualDate: row.inspectionActualDate,
        deliveryPackageActualDate: row.deliveryPackageActualDate,
        comments: row.comments == null ? '' : String(row.comments),
        updateBy: this.$store.state.user.name
      }))
      batchUpdateProofTrackingPlan({
        updateBy: this.$store.state.user.name,
        rows: payloadRows
      }).then(({ data }) => {
        if (data && data.code === 0) {
          this.$message.success('批量修改成功')
          this.batchEditMode = false
          this.getDataList()
        } else {
          this.$message.error(data.msg || '批量修改失败')
        }
      }).catch(() => {
        this.$message.error('批量修改异常')
      })
    },
    handleProcessDateChange (row, processCol, dateVal) {
      if (!row || !processCol) {
        return
      }
      const actualDate = this.formatDate(dateVal)
      // 工序日期被清空时，状态必须回退为“未完成”，避免出现“无日期但已完成”的脏状态。
      const nextStatus = actualDate ? '已完成' : '未完成'
      this.updateProcessFromTable(row, processCol, {
        status: nextStatus,
        actualDate: actualDate,
        remark: actualDate ? '表格直接选择日期' : '表格清空实际完成日期'
      }, true)
    },
    markProcessComplete (row, processCol) {
      if (!row || !processCol || this.isProcessComplete(row, processCol)) {
        return
      }
      const today = this.formatDate(new Date())
      this.updateProcessFromTable(row, processCol, {
        status: '已完成',
        actualDate: today,
        remark: '表格一键完成'
      }, false, '工序已标记为完成')
    },
    updateProcessFromTable (row, processCol, processData, silentSuccess, successMsg) {
      if (!row || !processCol || !row.trackingId) {
        this.$message.error('工序参数缺失')
        return
      }
      const status = processData && processData.status ? String(processData.status).trim() : ''
      if (!status) {
        this.$message.error('工序状态不能为空')
        return
      }
      const actualDate = processData && processData.actualDate ? this.formatDate(processData.actualDate) : ''
      const inData = {
        trackingId: row.trackingId,
        processCode: processCol.code,
        processName: processCol.label,
        status: status,
        actualDate: actualDate || null,
        remark: processData && processData.remark ? processData.remark : '',
        updateBy: this.$store.state.user.name
      }
      updateProofTrackingProcess(inData).then(({ data }) => {
        if (data && data.code === 0) {
          this.updateRowProcessValue(row, processCol, status, actualDate)
          const cacheKey = `${row.trackingId}_${processCol.code}`
          if (this.processHistoryMap[cacheKey]) {
            this.$delete(this.processHistoryMap, cacheKey)
          }
          if (!silentSuccess) {
            this.$message.success(successMsg || '工序更新成功')
          }
        } else {
          this.$message.error(data.msg || '工序更新失败')
          this.getDataList()
        }
      }).catch(() => {
        this.$message.error('工序更新异常')
        this.getDataList()
      })
    },
    openProcessDialog (row, processCol) {
      if (!row || !processCol) {
        return
      }
      this.currentRow = row
      this.processForm = {
        trackingId: row.trackingId,
        processCode: processCol.code,
        processName: processCol.label,
        status: row[processCol.statusField] || '未完成',
        actualDate: row[processCol.actualField] || '',
        remark: ''
      }
      this.processHistoryRows = []
      this.processDialogVisible = true
      this.loadProcessHistory(row, processCol, true)
    },
    submitProcessUpdate () {
      if (!this.processForm.trackingId || !this.processForm.processCode) {
        this.$message.error('参数缺失')
        return
      }
      if (!this.processForm.status) {
        this.$message.error('请选择状态')
        return
      }
      const inData = Object.assign({}, this.processForm, {
        updateBy: this.$store.state.user.name
      })
      this.saveProcessLoading = true
      updateProofTrackingProcess(inData).then(({ data }) => {
        this.saveProcessLoading = false
        if (data && data.code === 0) {
          this.$message.success('工序更新成功')
          this.processDialogVisible = false
          this.processHistoryMap = {}
          this.getDataList()
        } else {
          this.$message.error(data.msg || '工序更新失败')
        }
      }).catch(() => {
        this.saveProcessLoading = false
        this.$message.error('工序更新异常')
      })
    },
    finishProof (row) {
      const current = row || this.currentRow
      if (!current || !current.trackingId) {
        this.$message.warning('请先选择记录')
        return
      }
      this.finishForm = {
        trackingId: current.trackingId,
        actualityDeliveryDate: current.actualityDeliveryDate || this.formatDate(new Date())
      }
      this.finishDialogVisible = true
    },
    deleteProof (row) {
      const current = row || this.currentRow
      if (!current || !current.trackingId) {
        this.$message.warning('请先选择记录')
        return
      }
      this.$confirm('确定删除该打样记录吗？该操作仅删除打样记录，不会删除项目和项目物料。', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        deleteProofTracking({
          trackingId: current.trackingId,
          updateBy: this.$store.state.user.name
        }).then(({ data }) => {
          if (data && data.code === 0) {
            this.$message.success('打样记录删除成功')
            this.processHistoryMap = {}
            this.resetProcessTooltip()
            this.getDataList()
          } else {
            this.$message.error((data && data.msg) || '删除打样记录失败')
          }
        }).catch(() => {
          this.$message.error('删除打样记录异常')
        })
      }).catch(() => {})
    },
    submitFinishProof () {
      if (!this.finishForm.trackingId) {
        this.$message.warning('跟踪记录ID不能为空')
        return
      }
      if (!this.finishForm.actualityDeliveryDate) {
        this.$message.warning('请选择实际完成日期')
        return
      }
      this.finishSaveLoading = true
      finishProofTracking({
        trackingId: this.finishForm.trackingId,
        actualityDeliveryDate: this.finishForm.actualityDeliveryDate,
        updateBy: this.$store.state.user.name
      }).then(({ data }) => {
        this.finishSaveLoading = false
        if (data && data.code === 0) {
          this.$message.success('打样完成成功')
          this.finishDialogVisible = false
          this.getDataList()
        } else {
          this.$message.error(data.msg || '打样完成失败')
        }
      }).catch(() => {
        this.finishSaveLoading = false
        this.$message.error('打样完成异常')
      })
    },
    getProcessCacheKey (row, processCol) {
      if (!row || !processCol || !row.trackingId) {
        return ''
      }
      return `${row.trackingId}_${processCol.code}`
    },
    resetProcessTooltip () {
      this.processTooltipHoverKey = ''
      this.processTooltip.visible = false
      this.processTooltip.content = ''
    },
    hideProcessTooltip () {
      this.processTooltip.visible = false
      this.processTooltip.content = ''
    },
    updateProcessTooltipPosition (event) {
      if (!event) {
        return
      }
      const offsetX = 12
      const offsetY = 16
      let left = event.clientX + offsetX
      let top = event.clientY + offsetY
      const maxWidth = 220
      const maxHeight = 120
      if (left + maxWidth > window.innerWidth - 8) {
        left = window.innerWidth - maxWidth - 8
      }
      if (top + maxHeight > window.innerHeight - 8) {
        top = event.clientY - maxHeight - 8
      }
      this.processTooltip.left = Math.max(8, left)
      this.processTooltip.top = Math.max(8, top)
    },
    handleProcessTooltipMouseMove (event) {
      if (!this.processTooltip.visible) {
        return
      }
      this.updateProcessTooltipPosition(event)
    },
    handleProcessTooltipMouseLeave () {
      this.processTooltipHoverKey = ''
      this.hideProcessTooltip()
    },
    handleProcessTooltipMouseEnter (row, processCol, event) {
      const cacheKey = this.getProcessCacheKey(row, processCol)
      if (!cacheKey) {
        return
      }
      this.processTooltipHoverKey = cacheKey
      this.updateProcessTooltipPosition(event)
      const showTooltip = () => {
        if (this.processTooltipHoverKey !== cacheKey) {
          return
        }
        if (!this.hasProcessHistory(row, processCol)) {
          this.hideProcessTooltip()
          return
        }
        const content = this.getProcessTooltip(row, processCol)
        if (!content) {
          this.hideProcessTooltip()
          return
        }
        this.processTooltip.content = content
        this.processTooltip.visible = true
      }
      if (Object.prototype.hasOwnProperty.call(this.processHistoryMap, cacheKey)) {
        showTooltip()
        return
      }
      this.loadProcessHistory(row, processCol, false).then(() => {
        showTooltip()
      })
    },
    loadProcessHistory (row, processCol, refreshDialog) {
      if (!row || !row.trackingId) {
        return Promise.resolve([])
      }
      const cacheKey = this.getProcessCacheKey(row, processCol)
      if (Object.prototype.hasOwnProperty.call(this.processHistoryMap, cacheKey)) {
        const cachedRows = this.processHistoryMap[cacheKey] || []
        if (refreshDialog) {
          this.processHistoryRows = cachedRows
        }
        return Promise.resolve(cachedRows)
      }
      return queryProofTrackingProcessHistory({
        trackingId: row.trackingId,
        processCode: processCol.code
      }).then(({ data }) => {
        let rows = []
        if (data && data.code === 0) {
          rows = data.rows || []
          // Vue2 对对象新增 key 需要使用 $set 才能触发视图更新（tooltip 才会刷新历史列表）
          this.$set(this.processHistoryMap, cacheKey, rows)
          if (refreshDialog) {
            this.processHistoryRows = rows
          }
        }
        return rows
      }).catch(() => {
        if (refreshDialog) {
          this.processHistoryRows = []
        }
        this.$set(this.processHistoryMap, cacheKey, [])
        return []
      })
    },
    hasProcessHistory (row, processCol) {
      if (!row || !processCol || !row.trackingId) {
        return false
      }
      const cacheKey = `${row.trackingId}_${processCol.code}`
      const rows = this.processHistoryMap[cacheKey] || []
      return rows.length > 0
    },
    getProcessTooltip (row, processCol) {
      if (!row || !processCol) {
        return ''
      }
      const cacheKey = row.trackingId ? `${row.trackingId}_${processCol.code}` : ''
      const rows = this.processHistoryMap[cacheKey] || []

      const dateList = []
      const pushDate = (dateVal) => {
        const formatted = this.formatDate(dateVal)
        if (!formatted) {
          return
        }
        if (dateList.indexOf(formatted) === -1) {
          dateList.push(formatted)
        }
      }

      // 先放当前实际完成日期，再放历史变更日期（实际）
      pushDate(row[processCol.actualField])
      rows.forEach(item => {
        pushDate(item.newActualDate)
        pushDate(item.oldActualDate)
      })

      if (dateList.length === 0) {
        return ''
      }
      return dateList.join('\n')
    },
    isProcessComplete (row, processCol) {
      if (!row || !processCol) {
        return false
      }
      const statusVal = row[processCol.statusField]
      const status = statusVal ? String(statusVal).trim() : ''

      if (!status) {
        // 兼容历史数据：若没有状态但有实际完成日期，也按完成处理
        return !!this.formatDate(row[processCol.actualField])
      }
      if (status === '未完成' || status === '进行中') {
        return false
      }
      if (status === '已完成' || status === '打样完成') {
        return true
      }
      return status.indexOf('完成') > -1 && status.indexOf('未') === -1
    },
    getTrackingProcessColumnsByRow (row) {
      if (!row) {
        return []
      }
      const processColumnMap = {}
      const appendProcessColumn = (processCol) => {
        if (!processCol || !processCol.code || processColumnMap[processCol.code]) {
          return
        }
        processColumnMap[processCol.code] = processCol
      }
      this.processColumns.forEach(item => {
        appendProcessColumn(item)
      })
      // 兜底补齐默认工序元信息，避免配置变化导致状态字段找不到。
      DEFAULT_PROCESS_COLUMNS.forEach(item => {
        appendProcessColumn(item)
      })

      let orderedCodes = []
      if (this.getSelectedSearchBuNo()) {
        orderedCodes = this.visibleProcessColumns.map(item => item.code)
      } else {
        const configEntry = this.getBuProcessConfigEntry(row.buNo, row.site)
        const config = configEntry ? configEntry.config : null
        orderedCodes = config && Array.isArray(config.processOrderCodes) && config.processOrderCodes.length > 0
          ? this.getValidProcessOrderCodes(config.processOrderCodes)
          : this.getAllProcessCodes()
        const visibleCodes = this.getProcessVisibleCodesByBuNo(row.buNo, row.site)
        if (visibleCodes) {
          const visibleCodeMap = {}
          visibleCodes.forEach(code => {
            visibleCodeMap[code] = true
          })
          orderedCodes = orderedCodes.filter(code => visibleCodeMap[code])
        }
      }

      const result = []
      const usedCodeMap = {}
      orderedCodes.forEach(code => {
        const codeVal = code == null ? '' : String(code).trim()
        if (!codeVal || usedCodeMap[codeVal]) {
          return
        }
        const processCol = processColumnMap[codeVal]
        if (!processCol) {
          return
        }
        usedCodeMap[codeVal] = true
        result.push(processCol)
      })
      return result
    },
    getFirstPendingProcessLabel (row) {
      const processColumns = this.getTrackingProcessColumnsByRow(row)
      for (let i = 0; i < processColumns.length; i += 1) {
        const processCol = processColumns[i]
        if (!this.isProcessComplete(row, processCol)) {
          return processCol.label || processCol.code
        }
      }
      return '已全部完成'
    },
    getTrackingStatusType (row) {
      if (!row) {
        return ''
      }
      const requiredDeliveryDate = this.formatDate(row.requiredDeliveryDate)
      if (!requiredDeliveryDate) {
        return ''
      }
      const today = this.formatDate(new Date())
      // 按业务口径：今日 <= 预计完成日期 判定为 On schedule，其余判定为 Delay。
      if (today <= requiredDeliveryDate) {
        return 'onSchedule'
      }
      return 'delay'
    },
    getTrackingStatusText (row) {
      const statusType = this.getTrackingStatusType(row)
      if (!statusType) {
        return '-'
      }
      const statusPrefix = statusType === 'onSchedule' ? 'On schedule' : 'Delay'
      return `${statusPrefix}-${this.getFirstPendingProcessLabel(row)}`
    },
    getTrackingStatusClass (row) {
      const statusType = this.getTrackingStatusType(row)
      if (statusType === 'onSchedule') {
        return 'tracking-status tracking-status--on-schedule'
      }
      if (statusType === 'delay') {
        return 'tracking-status tracking-status--delay'
      }
      return 'tracking-status tracking-status--unknown'
    },
    formatDate (val) {
      if (!val) {
        return ''
      }
      if (typeof val === 'string') {
        return val.length > 10 ? val.substring(0, 10) : val
      }
      const date = new Date(val)
      if (isNaN(date.getTime())) {
        return ''
      }
      const y = date.getFullYear()
      const m = `${date.getMonth() + 1}`.padStart(2, '0')
      const d = `${date.getDate()}`.padStart(2, '0')
      return `${y}-${m}-${d}`
    },
    formatDateTime (val) {
      if (!val) {
        return ''
      }
      if (typeof val === 'string') {
        return val.length > 19 ? val.substring(0, 19) : val
      }
      const date = new Date(val)
      if (isNaN(date.getTime())) {
        return ''
      }
      const y = date.getFullYear()
      const m = `${date.getMonth() + 1}`.padStart(2, '0')
      const d = `${date.getDate()}`.padStart(2, '0')
      const hh = `${date.getHours()}`.padStart(2, '0')
      const mm = `${date.getMinutes()}`.padStart(2, '0')
      const ss = `${date.getSeconds()}`.padStart(2, '0')
      return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
    }
  }
}
</script>

<style >
.data-table .cell {
  line-height: 26px;
  font-size: 12px;
  height: 26px;
}

.search-form {
  margin-bottom: 0;
}

.button-group-col {
  width: 100%;
  margin-top: 16px;
}

.button-row {
  width: 100%;
  flex-wrap: wrap;
}

.bu-process-config-dialog .bu-process-order-list {
  display: grid;
  grid-template-columns: repeat(5, minmax(110px, 1fr));
  column-gap: 12px;
  row-gap: 8px;
  align-items: center;
}

.bu-process-config-dialog .bu-process-list-column .cell {
  white-space: normal !important;
  height: auto !important;
  line-height: 20px;
  padding-top: 6px;
  padding-bottom: 6px;
  overflow: hidden;
}

.bu-process-config-dialog .bu-process-list-column {
  vertical-align: top;
}

.bu-process-config-dialog .bu-process-order-item {
  display: flex;
  align-items: center;
  min-height: 28px;
  padding: 2px 6px;
  border: 1px solid transparent;
  border-radius: 4px;
  cursor: move;
  transition: border-color .2s ease, background-color .2s ease;
}

.bu-process-config-dialog .bu-process-order-item:hover {
  border-color: #7cc4ff;
  background: #f5fbff;
}

.bu-process-config-dialog .bu-process-order-item.is-dragging {
  opacity: .55;
  border-color: #409eff;
  background: #ecf5ff;
}

.bu-process-config-dialog .process-order-index {
  width: 22px;
  color: #606266;
  font-size: 12px;
  text-align: right;
  margin-right: 4px;
  flex-shrink: 0;
}

.bu-process-config-dialog .process-order-handle {
  margin-right: 6px;
  color: #909399;
  font-size: 14px;
}

.bu-process-config-dialog .bu-process-order-item .el-checkbox,
.bu-process-config-dialog .bu-process-order-item .el-checkbox + .el-checkbox {
  width: 100%;
  margin: 0 !important;
}

.bu-process-config-dialog .bu-process-order-item .el-checkbox__label {
  white-space: nowrap;
}

.bu-process-config-dialog .bu-process-config-tip {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 8px;
  padding: 6px 10px;
  border-radius: 4px;
  background: #f4f8ff;
  color: #5f6b7a;
  font-size: 12px;
}

.bu-process-config-dialog .bu-process-config-tip i {
  color: #409eff;
}

.button-col {
  margin-bottom: 8px;
}

.button-col .el-button {
  width: 100%;
}

.one-key-grid-form {
  padding: 0 8px;
}

.one-key-grid-form .el-form-item {
  margin-bottom: 10px;
}

.proof-grid-form {
  padding: 0 8px;
}

.proof-grid-form .el-form-item {
  margin-bottom: 10px;
}

.big-label a {
  color: #35b4b4;
}

.sync-text--ok {
  color: #67c23a;
  font-weight: 600;
}

.sync-text--pending {
  color: #e6a23c;
}

.tracking-status {
  width: 100%;
  min-height: 26px;
  padding: 4px 6px;
  box-sizing: border-box;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  background: #fff;
  color: #303133;
  font-weight: 500;
  line-height: 18px;
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tracking-status--on-schedule {
  background: #e8f5e9;
}

.tracking-status--delay {
  background: #fde2e2;
}

.tracking-status--unknown {
  background: #f4f4f5;
}

.process-cell {
  background: #fff;
  min-height: 44px;
  padding: 4px 2px;
}

.process-cell.is-complete {
  background: #ececec;
}

.process-cell.is-complete .link-date {
  color: #8c8c8c;
}

.process-cell.is-complete .link-date:hover,
.process-cell.is-complete .link-date:focus {
  color: #8c8c8c;
}

.process-cell--masked {
  background: transparent !important;
}

.process-content {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  text-align: center;
  line-height: 18px;
}

.process-complete-btn {
  padding: 4px 8px;
  min-width: 12px;
}

.comments-cell {
  display: flex;
  align-items: center;
  gap: 4px;
  min-height: 26px;
}

.comments-edit-input {
  flex: 1;
}

.comments-save-btn {
  padding: 4px 7px;
  min-width: 28px;
}

.comments-text {
  width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.process-date-tooltip {
  white-space: pre-line;
}

.process-history-tooltip {
  position: fixed;
  z-index: 3000;
  max-width: 220px;
  padding: 10px 12px;
  border-radius: 4px;
  background: rgba(48, 49, 51, 0.95);
  color: #fff;
  font-size: 12px;
  line-height: 1.5;
  white-space: pre-line;
  pointer-events: none;
}

.link-date {
  color: #0c4dbb;
  text-decoration: none;
}

.link-date:hover,
.link-date:focus {
  text-decoration: none;
}

.actual-date {
  margin-top: 2px;
  color: #666;
}

.finish-dialog-tip {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
  padding: 8px 10px;
  border-radius: 4px;
  background: #f4f8ff;
  color: #3a4b5f;
  font-size: 12px;
}

.finish-dialog-tip i {
  color: #409eff;
}

.finish-tip-highlight {
  font-size: 13px;
  font-weight: 700;
  color: #1f2d3d;
}

.finish-proof-dialog .el-dialog__body {
  padding-bottom: 8px;
}

.finish-form .el-form-item {
  margin-bottom: 14px;
}

.action-link {
  color: #0c4dbb;
  margin: 0 4px;
  cursor: pointer;
}

.edit-link {
  color: #e6a23c;
}

.delete-link {
  color: #f56c6c;
}

.end-link {
  color: #a303ff;
}
</style>

