You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

3745 lines
140 KiB

<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>