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.
 
 
 
 
 

6873 lines
258 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" filterable placeholder="请选择" style="width: 80px" @change="handleSearchBuChange">
<el-option
v-for="item in userBuList"
:key="`searchBu_${item.buNo}`"
:label="item.buDesc"
: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: 120px" />
</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: 120px" />
</el-form-item>
<el-form-item label="料号描述">
<el-input v-model="searchData.partDesc" clearable placeholder="请输入料号描述" style="width: 160px" />
</el-form-item>
<el-form-item v-if="isRfidBuNo(searchData.buNo)" label="分类">
<el-select v-model="searchData.dryWetType" clearable placeholder="全部" style="width: 100px">
<el-option v-for="item in dryWetTypeList" :key="`searchDryWet_${item}`" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="客户名称">
<el-select
v-model="searchData.customerNo"
filterable
clearable
remote
reserve-keyword
placeholder="请输入客户名称"
style="width: 120px"
:remote-method="handleSearchCustomerRemote"
:loading="searchCustomerOptionLoading"
@visible-change="handleSearchCustomerVisibleChange"
>
<el-option
v-for="item in searchCustomerOptions"
:key="`searchCustomer_${item.customerNo}`"
:label="getSearchCustomerOptionLabel(item)"
:value="item.customerNo"
/>
</el-select>
</el-form-item>
<el-form-item label="打样单号">
<el-input v-model="searchData.proofingNo" clearable placeholder="请输入打样单号" style="width: 120px" />
</el-form-item>
<el-form-item label="Sample making Year">
<el-select v-model="searchData.sampleMakingYear" clearable placeholder="全部" style="width: 110px">
<el-option
v-for="year in sampleMakingYearOptions"
:key="`searchSampleMakingYear_${year}`"
:label="year"
:value="year"
/>
</el-select>
</el-form-item>
<el-form-item label="Sample making Month">
<el-select v-model="searchData.sampleMakingMonth" clearable placeholder="全部" style="width: 90px">
<el-option
v-for="month in sampleMakingMonthOptions"
:key="`searchSampleMakingMonth_${month}`"
:label="month"
:value="month"
/>
</el-select>
</el-form-item>
<el-form-item label="PM inquery time">
<el-date-picker
v-model="searchData.pmInqueryTimeStart"
type="date"
value-format="yyyy-MM-dd"
placeholder="开始"
style="width: 120px"
/>
<span> - </span>
<el-date-picker
v-model="searchData.pmInqueryTimeEnd"
type="date"
value-format="yyyy-MM-dd"
placeholder="结束"
style="width: 120px"
/>
</el-form-item>
<el-form-item label="区域">
<el-select v-model="searchData.cProjectRegion" clearable placeholder="全部" style="width: 110px">
<el-option
v-for="i in cProjectRegionList"
:key="`searchRegion_${i.cProjectRegion}`"
:label="i.cProjectRegion"
:value="i.cProjectRegion"
/>
</el-select>
</el-form-item>
<el-form-item label="Engineer">
<el-select v-model="searchData.engineer" filterable clearable placeholder="全部" style="width: 130px">
<el-option
v-for="item in searchEngineerOptions"
:key="`searchEngineer_${item.value}`"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="PM/Sales">
<el-select v-model="searchData.projectManager" filterable clearable placeholder="全部" style="width: 130px">
<el-option
v-for="item in searchProjectManagerOptions"
:key="`searchProjectManager_${item.value}`"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="PIC">
<el-select v-model="searchData.pic" filterable clearable placeholder="全部" style="width: 130px">
<el-option
v-for="item in searchPicOptions"
:key="`searchPic_${item.value}`"
:label="item.label"
:value="item.value"
/>
</el-select>
</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
class="search-proofing-status-select"
v-model="searchData.proofingStatusList"
multiple
clearable
placeholder="全部"
style="width: 180px"
>
<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-button type="success" :loading="exportLoading" @click="exportTrackingData">导出</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="openCreateProofDialog()">新增打样</el-button>
<el-button type="success" plain :loading="syncBatchNpiLoading" @click="syncBatchProjectPartToNpi()">同步项目物料到NPI</el-button>
<el-button type="success" plain :loading="syncBatchProofLoading" @click="syncBatchProofToNpi()">同步打样到NPI</el-button>
<el-button
type="info"
v-if="isAuth('833001005:set')"
plain
:loading="buProcessConfigLoading"
:disabled="buProcessConfigLoading"
@click="openBuProcessConfigDialog"
>事业部进度设置</el-button>
<el-button type="info" v-if="isAuth('833001005:set2')" plain @click="openDefaultProcessConfigDialog">默认进度设置</el-button>
<el-tooltip
style="margin-top: 3px; margin-left: 10px"
placement="top">
<template slot="content">
<div>- 已完成后,双击日期选择框可回退为未完成</div>
<div>- 双击Comments可编辑,回车或点击✓保存</div>
</template>
<i class="el-icon-question default-process-tip-icon"></i>
</el-tooltip>
</el-row>
</el-form-item>
</el-form>
<el-table
ref="trackingTable"
:data="dataList"
border class="data-table"
stripe highlight-current-row
height="670"
v-loading="dataListLoading"
@sort-change="handleTrackingTableSortChange"
@selection-change="selectionChange"
@row-click="rowClick"
style="width: 100%; margin-top: 8px"
>
<el-table-column type="selection" width="25" />
<el-table-column
v-for="(item, trackingColumnIndex) in visibleTrackingColumns"
:key="`${item.columnProp}_${trackingColumnIndex}`"
:prop="item.columnProp"
:label="item.columnLabel"
:width="item.columnWidth"
:header-align="item.headerAlign || 'center'"
:align="item.align || 'left'"
:fixed="item.fixed === '' ? false : item.fixed"
:show-overflow-tooltip="item.showOverflowTooltip"
>
<template slot-scope="scope">
<span
v-if="isSyncIconTrackingColumn(item.columnProp)"
class="project-part-sync-text"
>
<i
v-if="isTrackingColumnSynced(scope.row, item.columnProp)"
class="el-icon-success project-part-sync-icon"
aria-hidden="true"
></i>
<a
v-if="canOpenNpiFromTracking(scope.row, item.columnProp)"
class="npi-jump-link"
href="javascript:void(0)"
@click.stop="openNpiFromTracking(item.columnProp, scope.row)"
>{{ getTrackingColumnDisplayValue(scope.row, item.columnProp) }}</a>
<span v-else>{{ getTrackingColumnDisplayValue(scope.row, item.columnProp) }}</span>
</span>
<a
v-else-if="item.columnProp === 'proofingStatus'"
:style="{ color: (scope.row.proofingStatus === '打样完成') ? '#67c23a' : '#046e97' }"
>{{ scope.row.proofingStatus || '进行中' }}</a>
<div
v-else-if="item.columnProp === 'trackingStatus'"
:class="getTrackingStatusClass(scope.row)"
>
{{ getTrackingStatusText(scope.row) }}
</div>
<span v-else-if="item.columnProp === 'engineer'">
{{ getTrackingRoleDisplayValue(scope.row, 'engineerName', 'engineer') }}
</span>
<span v-else-if="item.columnProp === 'projectManager'">
{{ getTrackingRoleDisplayValue(scope.row, 'projectManagerName', 'projectManager') }}
</span>
<span v-else-if="item.columnProp === 'pic'">
{{ getTrackingRoleDisplayValue(scope.row, 'projectOwnerName', 'pic') }}
</span>
<span
v-else-if="item.columnProp === 'deliveryVariance'"
:class="{ 'delivery-variance-alert': isDeliveryVarianceAlert(scope.row) }"
>{{ getTrackingColumnDisplayValue(scope.row, item.columnProp) }}</span>
<span v-else>{{ getTrackingColumnDisplayValue(scope.row, item.columnProp) }}</span>
</template>
</el-table-column>
<el-table-column
v-for="(item, processColumnIndex) in visibleProcessColumns"
:key="`${item.code}_${item.sortNo || ''}_${processColumnIndex}`"
:prop="item.actualField"
:label="item.label"
:width="getProcessColumnWidth(item)"
sortable="custom"
align="center"
header-align="center"
>
<template slot-scope="scope">
<div
v-if="isProcessColumnVisibleForRow(scope.row, item)"
:class="['process-cell', isProcessStatusComplete(scope.row, item) ? 'is-complete' : '']"
>
<div
:class="['process-content', isProcessStatusComplete(scope.row, item) ? 'is-complete' : '']"
@mouseenter="handleProcessTooltipMouseEnter(scope.row, item, $event)"
@mousemove="handleProcessTooltipMouseMove($event)"
@mouseleave="handleProcessTooltipMouseLeave"
>
<template v-if="isTrackingRowProcessReadonly(scope.row)">
<div class="process-readonly-content">
<span class="process-date-display">{{ scope.row[item.actualField] || '' }}</span>
<el-button
v-if="canRollbackProofingComplete(scope.row, item)"
class="process-rollback-btn process-rollback-btn--readonly"
type="warning"
size="mini"
plain
title="撤回Delivery&Package已完成"
@click.stop="rollbackProcessComplete(scope.row, item)"
>↶</el-button>
</div>
</template>
<template v-else>
<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)"
@dblclick.native.stop="handleProcessDateDoubleClick(scope.row, item)"
/>
<el-button
v-if="!isProcessStatusComplete(scope.row, item)"
class="process-complete-btn"
type="success"
size="mini"
plain
title="标记为已完成"
@click.stop="markProcessComplete(scope.row, item)"
>✓</el-button>
</template>
</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="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="80" align="center">
<template slot-scope="scope">
<a @click="openOneKeyDialog('edit', scope.row)" v-if="scope.row.proofingStatus !== '打样完成'">修改</a>
<a class="action-link detail-link" @click="openOneKeyDialog('detail', scope.row)" v-if="scope.row.proofingNo && scope.row.proofingStatus === '打样完成'">详情</a>
<a class="action-link delete-link" v-if="scope.row.proofingStatus === '草稿'" @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="1180px"
:close-on-click-modal="false"
custom-class="bu-process-config-dialog"
>
<div class="bu-process-config-tip">
<i class="el-icon-info"></i>
<span>拖动可调整顺序(按序号从左到右),勾选控制可见列;工序分类按 BU 独立维护</span>
</div>
<el-table :data="buProcessConfigRows" border stripe max-height="560">
<el-table-column prop="buDesc" label="BU" width="120" show-overflow-tooltip />
<el-table-column label="可见进度列表 / 工序分类" min-width="760" 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>
<el-select
class="bu-process-category-select"
:value="getBuProcessCategoryCode(scope.row, processCode)"
size="mini"
clearable
filterable
placeholder="未分类"
@change="handleBuProcessCategoryChange(scope.row, processCode, $event)"
@mousedown.native.stop
@click.native.stop
>
<el-option
v-for="categoryItem in getProcessCategoryOptionsByBuNo(scope.row.buNo)"
:key="`buProcessCategory_${scope.row.buNo}_${categoryItem.categoryCode}`"
:label="`${categoryItem.categoryName}`"
:value="categoryItem.categoryCode"
/>
</el-select>
</div>
</el-checkbox-group>
</template>
</el-table-column>
<el-table-column label="分类维护" width="100" align="center">
<template slot-scope="scope">
<a type="text" @click="openProcessCategoryDialog(scope.row)">维护分类</a>
</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="默认进度设置"
:visible.sync="defaultProcessConfigDialogVisible"
width="600px" class="zxClass"
:close-on-click-modal="false"
custom-class="default-process-config-dialog"
@opened="initDefaultProcessConfigSortable"
@closed="handleDefaultProcessConfigDialogClosed"
>
<div class="default-process-config-actions">
<el-button type="success" plain size="mini" @click="addDefaultProcessConfigRow">新增一行</el-button>
<el-button
type="danger"
plain
size="mini"
:disabled="defaultProcessConfigSelectionRows.length === 0 || defaultProcessConfigDeleteLoading"
:loading="defaultProcessConfigDeleteLoading"
@click="deleteDefaultProcessConfigRow"
>删除选中</el-button>
</div>
<el-table
ref="defaultProcessConfigTable"
:data="defaultProcessConfigRows"
row-key="_rowKey"
border
stripe
max-height="640"
@selection-change="handleDefaultProcessConfigSelectionChange"
>
<el-table-column type="selection" width="50" />
<el-table-column label="排序" width="90" align="center">
<template slot-scope="scope">
<i class="el-icon-rank default-process-drag-handle"></i>
<span class="default-process-order-index">{{ scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column label="工序编码" min-width="160">
<template slot-scope="scope">
<el-input v-model.trim="scope.row.code" size="mini" clearable placeholder="如: coating" />
</template>
</el-table-column>
<el-table-column label="工序名称" min-width="180">
<template slot-scope="scope">
<el-input v-model.trim="scope.row.label" size="mini" clearable placeholder="如: Coating" />
</template>
</el-table-column>
<!-- <el-table-column label="启用" width="100" align="center">
<template slot-scope="scope">
<el-select v-model="scope.row.activeFlag" size="mini" style="width: 80px;">
<el-option label="是" value="Y" />
<el-option label="否" value="N" />
</el-select>
</template>
</el-table-column>-->
</el-table>
<div v-if="defaultProcessConfigRows.length === 0" class="default-process-config-empty">
当前没有默认行,请点击“新增一行”后填写并保存。
</div>
<div slot="footer">
<el-button @click="defaultProcessConfigDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="saveDefaultProcessConfig">保存设置</el-button>
</div>
</el-dialog>
<el-dialog
:title="`工序分类维护(${processCategoryDialogBuDesc || processCategoryDialogBuNo || '-'})`"
:visible.sync="processCategoryDialogVisible"
width="450px"
:close-on-click-modal="false"
custom-class="process-category-config-dialog"
@closed="handleProcessCategoryDialogClosed"
>
<div class="default-process-config-actions">
<el-button type="success" plain size="mini" @click="addProcessCategoryRow">新增一行</el-button>
<el-button
type="danger"
plain
size="mini"
:disabled="processCategorySelectionRows.length === 0 || processCategorySaveLoading"
:loading="processCategorySaveLoading"
@click="deleteProcessCategoryRows"
>删除选中</el-button>
</div>
<el-table
ref="processCategoryTable"
:data="processCategoryRows"
row-key="_rowKey"
border
stripe class="zxClass"
max-height="520"
@selection-change="handleProcessCategorySelectionChange"
>
<el-table-column type="selection" width="50" />
<el-table-column label="分类名称" min-width="280">
<template slot-scope="scope">
<el-input v-model.trim="scope.row.categoryName" size="mini" clearable placeholder="如: 制前工序" />
</template>
</el-table-column>
</el-table>
<div v-if="processCategoryRows.length === 0" class="default-process-config-empty">
当前没有分类,请点击“新增一行”后填写并保存。
</div>
<div slot="footer">
<el-button @click="processCategoryDialogVisible = false">关闭</el-button>
<el-button type="primary" :loading="processCategorySaveLoading" @click="saveProcessCategoryConfig">保存分类</el-button>
</div>
</el-dialog>
<el-dialog :title="oneKeyDialogTitle" :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%" @change="handleOneKeyBuChange">
<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="9" v-show="isOneKeyCreateFieldVisible('projectNo')">
<el-form-item label="项目编码">
<el-autocomplete
v-model.trim="oneKeyForm.projectNo"
:fetch-suggestions="queryOneKeyProjectSuggestions"
:trigger-on-focus="true"
placeholder="输入项目描述模糊搜索"
style="width: 100%"
@select="handleOneKeyProjectSelect"
@blur="handleOneKeyProjectNoBlur">
<template slot-scope="{ item }">
<div class="one-key-suggest-sub">
{{ item.value }} | {{ item.customerDesc || '-' }}
</div>
<div class="one-key-suggest-main">{{ item.projectDesc || item.value }}</div>
</template>
</el-autocomplete>
</el-form-item>
</el-col>
<el-col :span="9" 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="!isOneKeyDetailMode" 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-autocomplete
v-model.trim="oneKeyForm.testPartNo"
:fetch-suggestions="queryOneKeyPartSuggestions"
:trigger-on-focus="true"
placeholder="输入物料描述模糊搜索"
style="width: 100%"
@select="handleOneKeyPartSelect"
@blur="handleOneKeyPartNoBlur">
<template slot-scope="{ item }">
<div class="one-key-suggest-sub">{{ item.value }}</div>
<div class="one-key-suggest-main">{{ item.partDesc || item.value }}</div>
</template>
</el-autocomplete>
</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="isOneKeyDryWetTypeVisible()?3: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-col :span="3" v-show="isOneKeyCreateFieldVisible('dryWetType') && isOneKeyDryWetTypeVisible()">
<el-form-item label="分类">
<el-select v-model="oneKeyForm.dry_wet_type" placeholder="请选择" clearable style="width: 100%">
<el-option v-for="item in dryWetTypeList" :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="!isOneKeyDetailMode" 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="getBaseList(2008)">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="getBaseList(2006)">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="getBaseList(2007)">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="getBaseList(2010)">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="getBaseList(2012)">前道工程师</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 disabled v-model="oneKeyForm.proofingStatus" clearable style="width: 100%">
<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('requiredDeliveryDate')">
<el-form-item label="预计完成日期">
<el-date-picker
v-model="oneKeyForm.requiredDeliveryDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="选择日期"
style="width: 100%"
@change="handleOneKeyRequiredDeliveryDateChange">
</el-date-picker>
</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('proofingNumber')">
<el-form-item label="数量">
<el-input v-model.number="oneKeyForm.proofingNumber" style="width: 100%"></el-input>
<div v-if="oneKeySelectedApplyOption" class="apply-qty-reference">
试验申请数量:{{ getApplyQuantityDisplayText(oneKeySelectedApplyOption.applyQuantity) }}
</div>
</el-form-item>
</el-col>
<el-col :span="6" v-show="isOneKeyCreateFieldVisible('pmInqueryTime')">
<el-form-item label="PM inquery time">
<el-date-picker
v-model="oneKeyForm.pmInqueryTime"
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('baseline')">
<el-form-item label="Baseline">
<el-date-picker
v-model="oneKeyForm.baseline"
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) - (isOneKeyCreateFieldVisible('baseline') ? 6 : 0) - (isOneKeyCreateFieldVisible('pmInqueryTime') ? 6 : 0)" v-show="isOneKeyCreateFieldVisible('remark')">
<el-form-item label="备注">
<el-input 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">{{ isOneKeyDetailMode ? '关闭' : '取消' }}</el-button>
<el-button v-if="!isOneKeyDetailMode" 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-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="打样状态">
<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-input v-model="proofDialogData.proofingNumber" style="width: 100%" />
<div v-if="proofDialogSelectedApplyOption" class="apply-qty-reference">
试验申请数量:{{ getApplyQuantityDisplayText(proofDialogSelectedApplyOption.applyQuantity) }}
</div>
</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="6">
<el-form-item label="PM inquery time">
<el-date-picker
v-model="proofDialogData.pmInqueryTime"
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="备注">
<el-input v-model="proofDialogData.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="proofDialogVisible = false">取 消</el-button>
<el-button type="primary" :loading="proofSaveLoading" @click="saveProofRecord">保 存</el-button>
</el-footer>
</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 {
createProofTrackingRecord,
deleteProofTracking,
exportProofTracking,
finishProofTracking,
oneKeyCreateProofTracking,
oneKeyUpdateProofTracking,
queryProofTrackingBuProcessConfig,
queryProofTrackingProcessColumns,
queryProofTrackingProcessCategories,
deleteProofTrackingProcessColumnsManage,
queryProofTrackingProcessColumnsManage,
queryProofTrackingProcessHistory,
saveProofTrackingBuProcessConfig,
saveProofTrackingProcessCategoriesManage,
saveProofTrackingProcessColumnsManage,
searchProjectInfoTracking,
searchProjectPartTracking,
searchProofTracking,
queryProofTrackingCustomerOptions,
batchSyncProofTracking,
batchSyncProjectPartTracking,
updateProofTrackingComments,
updateProofTrackingProcess
} from '@/api/sampleTracking/sampleTracking'
import { searchExpApplyList } from '@/api/erf/erf'
import { eamProjectInfoSearch, eamProjectPartSearch, 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 { getTableDefaultListLanguage, getTableUserListLanguage } from '@/api/table.js'
import Chooselist from '@/views/modules/common/Chooselist_eam'
import Sortable from 'sortablejs'
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', processCategoryCode: '', processCategoryName: '' },
{ label: 'Cylinder/Cutter', code: 'cylinderCutter', planField: 'cylinderCutterPlanDate', actualField: 'cylinderCutterActualDate', statusField: 'cylinderCutterStatus', processCategoryCode: '', processCategoryName: '' },
{ label: 'Lamination', code: 'lamination', planField: 'laminationPlanDate', actualField: 'laminationActualDate', statusField: 'laminationStatus', processCategoryCode: '', processCategoryName: '' },
{ label: 'Printing', code: 'printing', planField: 'printingPlanDate', actualField: 'printingActualDate', statusField: 'printingStatus', processCategoryCode: '', processCategoryName: '' },
{ label: 'Etching', code: 'etching', planField: 'etchingPlanDate', actualField: 'etchingActualDate', statusField: 'etchingStatus', processCategoryCode: '', processCategoryName: '' },
{ label: 'Slitting&Packing', code: 'slittingPacking', planField: 'slittingPackingPlanDate', actualField: 'slittingPackingActualDate', statusField: 'slittingPackingStatus', processCategoryCode: '', processCategoryName: '' },
{ label: 'Bonding', code: 'bonding', planField: 'bondingPlanDate', actualField: 'bondingActualDate', statusField: 'bondingStatus', processCategoryCode: '', processCategoryName: '' },
{ label: 'CL60/CL822', code: 'cl60Cl822', planField: 'cl60Cl822PlanDate', actualField: 'cl60Cl822ActualDate', statusField: 'cl60Cl822Status', processCategoryCode: '', processCategoryName: '' },
{ label: 'Spotting', code: 'spotting', planField: 'spottingPlanDate', actualField: 'spottingActualDate', statusField: 'spottingStatus', processCategoryCode: '', processCategoryName: '' },
{ label: 'Encoding', code: 'encoding', planField: 'encodingPlanDate', actualField: 'encodingActualDate', statusField: 'encodingStatus', processCategoryCode: '', processCategoryName: '' },
{ label: 'Die cut', code: 'dieCut', planField: 'dieCutPlanDate', actualField: 'dieCutActualDate', statusField: 'dieCutStatus', processCategoryCode: '', processCategoryName: '' },
{ label: 'Inspection', code: 'inspection', planField: 'inspectionPlanDate', actualField: 'inspectionActualDate', statusField: 'inspectionStatus', processCategoryCode: '', processCategoryName: '' },
{ label: 'Delivery&Package', code: 'deliveryPackage', planField: 'deliveryPackagePlanDate', actualField: 'deliveryPackageActualDate', statusField: 'deliveryPackageStatus', processCategoryCode: '', processCategoryName: '' }
]
function cloneDefaultProcessColumns () {
return DEFAULT_PROCESS_COLUMNS.map(item => Object.assign({}, item))
}
function buildProcessCodeLabelMapByRows (rows) {
const nextMap = {}
if (!Array.isArray(rows)) {
return nextMap
}
rows.forEach(item => {
if (!item) {
return
}
const code = item.code == null ? '' : String(item.code).trim()
const label = item.label == null ? '' : String(item.label).trim()
if (!code || !label) {
return
}
// 同时缓存原始编码与小写编码,兼容历史配置大小写不一致的场景。
nextMap[code] = label
nextMap[code.toLowerCase()] = label
})
return nextMap
}
const ONE_KEY_ROLE_FIELDS = [
'projectManager',
'projectOwner',
'engineer',
'cManufactureEngineer',
'cQualityEngineer1',
'cQualityEngineer2',
'cQualityEngineer3',
'cQualityEngineer4',
'cQualityEngineer5',
'cQualityEngineer6',
'cQualityEngineer7',
'ipqcHardTag',
'docEngineer',
'docEngineer2'
]
const DEFAULT_TRACKING_COLUMNS = [
{
serialNumber: 'proofTrackingTable1ProjectNo',
columnProp: 'projectNo',
headerAlign: 'center',
align: 'left',
columnLabel: '项目编码',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 90,
showOverflowTooltip: true
},
{
serialNumber: 'proofTrackingTable1ProjectDesc',
columnProp: 'projectDesc',
headerAlign: 'center',
align: 'left',
columnLabel: '项目描述',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 120,
showOverflowTooltip: true
},
{
serialNumber: 'proofTrackingTable1TestPartNo',
columnProp: 'testPartNo',
headerAlign: 'center',
align: 'left',
columnLabel: '项目物料',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 90,
showOverflowTooltip: true
},
{
serialNumber: 'proofTrackingTable1PartDesc',
columnProp: 'partDesc',
headerAlign: 'center',
align: 'left',
columnLabel: '物料描述',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: 'left',
columnWidth: 150,
showOverflowTooltip: true
},
{
serialNumber: 'proofTrackingTable1DryWetType',
columnProp: 'dryWetType',
headerAlign: 'center',
align: 'center',
columnLabel: '分类',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 70,
showOverflowTooltip: true
},
{
serialNumber: 'proofTrackingTable1ProjectRegion',
columnProp: 'cProjectRegion',
headerAlign: 'center',
align: 'left',
columnLabel: '区域',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 90,
showOverflowTooltip: true
},
{
serialNumber: 'proofTrackingTable1CustomerDesc',
columnProp: 'customerDesc',
headerAlign: 'center',
align: 'left',
columnLabel: '客户名称',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 130,
showOverflowTooltip: true
},
{
serialNumber: 'proofTrackingTable1Engineer',
columnProp: 'engineer',
headerAlign: 'center',
align: 'left',
columnLabel: 'Engineer',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 120,
showOverflowTooltip: true
},
{
serialNumber: 'proofTrackingTable1ProjectManager',
columnProp: 'projectManager',
headerAlign: 'center',
align: 'left',
columnLabel: 'PM/Sales',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 120,
showOverflowTooltip: true
},
{
serialNumber: 'proofTrackingTable1ProofingNo',
columnProp: 'proofingNo',
headerAlign: 'center',
align: 'left',
columnLabel: '打样单号',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 90,
showOverflowTooltip: true
},
{
serialNumber: 'proofTrackingTable1ProofingStatus',
columnProp: 'proofingStatus',
headerAlign: 'center',
align: 'center',
columnLabel: '打样状态',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 80,
showOverflowTooltip: false
},
// Sample Qty / Year / Month / LT 为计算列;仅 PM inquery time 落库。
{
serialNumber: 'proofTrackingTable1SampleQty',
columnProp: 'sampleQty',
headerAlign: 'center',
align: 'center',
columnLabel: 'Sample Qty',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 90,
showOverflowTooltip: false
},
{
serialNumber: 'proofTrackingTable1PmInqueryTime',
columnProp: 'pmInqueryTime',
headerAlign: 'center',
align: 'center',
columnLabel: 'PM inquery time',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 120,
showOverflowTooltip: false
},
{
serialNumber: 'proofTrackingTable1SampleMakingYear',
columnProp: 'sampleMakingYear',
headerAlign: 'center',
align: 'center',
columnLabel: 'Sample making Year',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 90,
showOverflowTooltip: false
},
{
serialNumber: 'proofTrackingTable1SampleMakingMonth',
columnProp: 'sampleMakingMonth',
headerAlign: 'center',
align: 'center',
columnLabel: 'Sample making Month',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 90,
showOverflowTooltip: false
},
{
serialNumber: 'proofTrackingTable1SampleLt',
columnProp: 'sampleLt',
headerAlign: 'center',
align: 'center',
columnLabel: 'Sample LT',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 90,
showOverflowTooltip: false
},
{
serialNumber: 'proofTrackingTable1SampleLtInquery',
columnProp: 'sampleLtInquery',
headerAlign: 'center',
align: 'center',
columnLabel: 'Sample LT(Inquery)',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 140,
showOverflowTooltip: false
},
{
serialNumber: 'proofTrackingTable1Baseline',
columnProp: 'baseline',
headerAlign: 'center',
align: 'center',
columnLabel: 'Baseline',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 100,
showOverflowTooltip: false
},
{
serialNumber: 'proofTrackingTable1DeliveryVariance',
columnProp: 'deliveryVariance',
headerAlign: 'center',
align: 'center',
columnLabel: 'Delivery Variance',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 110,
showOverflowTooltip: false
},
{
serialNumber: 'proofTrackingTable1TrackingStatus',
columnProp: 'trackingStatus',
headerAlign: 'center',
align: 'center',
columnLabel: 'Tracking Status',
columnHidden: false,
columnImage: false,
columnSortable: false,
status: true,
fixed: '',
columnWidth: 150,
showOverflowTooltip: true
}
]
function cloneDefaultTrackingColumns () {
return DEFAULT_TRACKING_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: '',
dryWetType: '',
customerNo: '',
proofingNo: '',
cProjectRegion: '',
engineer: '',
projectManager: '',
pic: '',
projectPartSyncFlag: '',
proofSyncFlag: '',
proofingStatusList: [],
sampleQty: '',
sampleMakingYear: '',
sampleMakingMonth: '',
pmInqueryTimeStart: '',
pmInqueryTimeEnd: ''
},
searchCustomerOptions: [],
searchCustomerOptionLoading: false,
searchCustomerQueryToken: 0,
searchCustomerOptionInitialized: false,
searchEngineerOptions: [],
searchProjectManagerOptions: [],
searchPicOptions: [],
trackingColumnList: cloneDefaultTrackingColumns(),
hasUserTrackingColumnConfig: false,
processColumns: cloneDefaultProcessColumns(),
processCodeLabelMap: buildProcessCodeLabelMapByRows(cloneDefaultProcessColumns()),
trackingTableSortProp: '',
trackingTableSortOrder: '',
buProcessCategoryOptionsMap: {},
buProcessConfigLoading: false,
buProcessConfigDialogVisible: false,
buProcessConfigRows: [],
buProcessConfigMap: {},
defaultProcessConfigDialogVisible: false,
defaultProcessConfigRows: [],
defaultProcessConfigSelectionRows: [],
defaultProcessConfigDeleteLoading: false,
defaultProcessConfigRowSeed: 0,
defaultProcessConfigSortable: null,
processCategoryDialogVisible: false,
processCategoryDialogBuNo: '',
processCategoryDialogBuDesc: '',
processCategoryRows: [],
processCategorySelectionRows: [],
processCategorySaveLoading: false,
processCategoryRowSeed: 0,
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'],
dryWetTypeList: ['Dry', 'Wet', '编码打印'],
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',
'dryWetType',
'projectManager',
'projectOwner',
'engineer',
'cManufactureEngineer',
'cQualityEngineer6',
'cQualityEngineer1',
'cQualityEngineer2',
'cQualityEngineer4',
'cQualityEngineer3',
'cQualityEngineer5',
'ipqcHardTag',
'cQualityEngineer7',
'docEngineer',
'docEngineer2',
'projectStatus',
'projectPhase',
'proofingNo',
'proofingStatus',
'proofingNumber',
'baseline',
'planStartDate',
'pmInqueryTime',
'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' },
// 以下角色与 NPI 项目页一致,走 Chooselist tag,不能复用文档工程师的 searchBusinessInfo7。
cQualityEngineer4: { tagNo: 2006, title: 'SQE', nameField: 'cQualityEngineer4Name' },
cQualityEngineer5: { tagNo: 2007, title: 'FQC2', nameField: 'cQualityEngineer5Name' },
cQualityEngineer6: { tagNo: 2008, title: 'IQC', nameField: 'cQualityEngineer6Name' },
docEngineer: { apiKey: '7', title: '文档工程师', nameField: 'docEngineerName' },
ipqcHardTag: { tagNo: 2010, title: 'IPQC-Hardtag', nameField: 'ipqcHardTagName' },
cQualityEngineer7: { tagNo: 2012, title: '前道工程师', nameField: 'cQualityEngineer7Name' },
docEngineer2: { apiKey: '8', title: '文档工程师2', nameField: 'docEngineer2Name' }
},
dataList: [],
dataListLoading: false,
exportLoading: false,
syncBatchNpiLoading: false,
syncBatchProofLoading: false,
pageIndex: 1,
pageSize: 50,
totalPage: 0,
currentRow: null,
manualSelectedTrackingId: null,
selectionRows: [],
processHistoryMap: {},
processTooltipHoverKey: '',
processTooltip: {
visible: false,
content: '',
left: 0,
top: 0
},
commentsEditingTrackingId: null,
commentsDraft: '',
commentsSaveLoading: false,
oneKeyDialogVisible: false,
oneKeyDialogMode: 'create',
saveOneKeyLoading: false,
oneKeyForm: {},
oneKeyProofingApplyOptions: [],
oneKeyProofingApplyLoading: false,
oneKeyProofingApplyQueryToken: 0,
oneKeyProjectQueryToken: 0,
oneKeyPartQueryToken: 0,
oneKeyProjectNoSnapshot: '',
oneKeyPartNoSnapshot: '',
oneKeyProjectRoleBaseline: {},
proofDialogVisible: false,
proofSaveLoading: false,
proofDialogData: this.getDefaultProofDialogData(),
proofDialogApplyOptions: [],
proofDialogApplyLoading: false,
proofDialogApplyQueryToken: 0,
finishDialogVisible: false,
finishSaveLoading: false,
finishForm: {
trackingId: null,
actualityDeliveryDate: ''
}
}
},
activated () {
this.loadUserBuList().then(() => {
return Promise.all([
this.queryProcessColumns(),
this.loadTrackingColumns(),
this.loadSearchRoleOptions()
])
}).then(() => {
this.queryBuProcessConfig()
this.getDataList()
})
},
deactivated () {
this.destroyDefaultProcessConfigSortable()
},
computed: {
visibleTrackingColumns () {
const sourceColumns = this.hasUserTrackingColumnConfig
? (Array.isArray(this.trackingColumnList) ? this.trackingColumnList : [])
: ((Array.isArray(this.trackingColumnList) && this.trackingColumnList.length > 0)
? this.trackingColumnList
: this.buildDefaultTrackingColumns())
return sourceColumns.filter(item => this.isTrackingColumnVisible(item))
},
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
},
isOneKeyDetailMode () {
return this.oneKeyDialogMode === 'detail'
},
oneKeyDialogTitle () {
if (this.oneKeyDialogMode === 'edit') {
return '修改项目/物料/打样'
}
if (this.oneKeyDialogMode === 'detail') {
return '项目/物料/打样详情'
}
return '一键创建项目/物料/打样'
},
oneKeySelectedApplyOption () {
const selectedApplyNo = this.oneKeyForm && !this.isBlankValue(this.oneKeyForm.proofingNo)
? String(this.oneKeyForm.proofingNo).trim()
: ''
if (!selectedApplyNo) {
return null
}
return this.oneKeyProofingApplyOptions.find(item => item && item.applyNo === selectedApplyNo) || null
},
proofDialogSelectedApplyOption () {
const selectedApplyNo = this.proofDialogData && !this.isBlankValue(this.proofDialogData.proofingNo)
? String(this.proofDialogData.proofingNo).trim()
: ''
if (!selectedApplyNo) {
return null
}
return this.proofDialogApplyOptions.find(item => item && item.applyNo === selectedApplyNo) || null
},
sampleMakingYearOptions () {
const currentYear = new Date().getFullYear()
const years = []
for (let year = currentYear; year >= 2022; year--) {
years.push(year)
}
return years
},
sampleMakingMonthOptions () {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
}
},
methods: {
getTrackingColumnFunctionId () {
return this.$route && this.$route.meta && this.$route.meta.menuId
? this.$route.meta.menuId
: ''
},
getTrackingColumnTableId () {
const functionId = this.getTrackingColumnFunctionId()
return functionId ? `${functionId}table1` : 'projectProofTrackingTable1'
},
buildDefaultTrackingColumns () {
const functionId = this.getTrackingColumnFunctionId()
const tableId = this.getTrackingColumnTableId()
const userId = this.$store.state.user.name
return cloneDefaultTrackingColumns().map(item => Object.assign({}, item, {
userId: userId,
functionId: functionId,
tableId: tableId,
tableName: '项目打样跟踪'
}))
},
normalizeTrackingColumnWidth (value, fallbackValue) {
const parsedValue = parseInt(value, 10)
if (!Number.isNaN(parsedValue) && parsedValue > 0) {
return parsedValue
}
const parsedFallback = parseInt(fallbackValue, 10)
return !Number.isNaN(parsedFallback) && parsedFallback > 0 ? parsedFallback : 120
},
normalizeTrackingColumnBoolean (value, fallbackValue) {
if (value === undefined || value === null || value === '') {
return fallbackValue
}
if (typeof value === 'boolean') {
return value
}
if (typeof value === 'number') {
return value !== 0
}
const normalizedValue = String(value).trim().toLowerCase()
if (normalizedValue === 'true' || normalizedValue === '1' || normalizedValue === 'y' || normalizedValue === 'yes') {
return true
}
if (normalizedValue === 'false' || normalizedValue === '0' || normalizedValue === 'n' || normalizedValue === 'no') {
return false
}
return fallbackValue
},
normalizeTrackingColumnItem (item, fallbackItem) {
const defaultItem = fallbackItem || {}
const mergedItem = Object.assign({}, defaultItem, item || {})
const rawColumnProp = mergedItem.columnProp == null ? '' : String(mergedItem.columnProp).trim()
const columnProp = rawColumnProp.toLowerCase() === 'pic' ? 'pic' : rawColumnProp
const normalizedLabel = mergedItem.columnLabel == null ? '' : String(mergedItem.columnLabel).trim()
return Object.assign({}, mergedItem, {
columnProp: columnProp,
columnLabel: normalizedLabel || defaultItem.columnLabel || columnProp,
headerAlign: mergedItem.headerAlign || defaultItem.headerAlign || 'center',
align: mergedItem.align || defaultItem.align || 'left',
fixed: mergedItem.fixed == null ? (defaultItem.fixed || '') : mergedItem.fixed,
columnWidth: this.normalizeTrackingColumnWidth(mergedItem.columnWidth, defaultItem.columnWidth),
showOverflowTooltip: this.normalizeTrackingColumnBoolean(mergedItem.showOverflowTooltip, defaultItem.showOverflowTooltip !== false),
status: this.normalizeTrackingColumnBoolean(mergedItem.status, true),
columnHidden: this.normalizeTrackingColumnBoolean(mergedItem.columnHidden, false)
})
},
normalizeTrackingColumns (rows, appendDefaultColumns) {
const defaultColumns = this.buildDefaultTrackingColumns()
const defaultColumnMap = {}
defaultColumns.forEach(item => {
defaultColumnMap[item.columnProp] = item
})
const mergedRows = []
const usedColumnPropMap = {}
if (Array.isArray(rows)) {
rows.forEach(item => {
const columnProp = item && item.columnProp != null ? String(item.columnProp).trim() : ''
if (!columnProp || usedColumnPropMap[columnProp]) {
return
}
usedColumnPropMap[columnProp] = true
mergedRows.push(this.normalizeTrackingColumnItem(item, defaultColumnMap[columnProp]))
})
}
if (appendDefaultColumns !== true) {
return mergedRows
}
// 用户旧配置可能缺少新加列,这里按系统默认顺序补齐,避免新增列永远看不到。
defaultColumns.forEach((defaultItem, defaultIndex) => {
if (usedColumnPropMap[defaultItem.columnProp]) {
return
}
let insertIndex = mergedRows.length
for (let i = defaultIndex + 1; i < defaultColumns.length; i++) {
const nextProp = defaultColumns[i].columnProp
const matchedIndex = mergedRows.findIndex(item => item.columnProp === nextProp)
if (matchedIndex > -1) {
insertIndex = matchedIndex
break
}
}
if (insertIndex === mergedRows.length) {
for (let i = defaultIndex - 1; i >= 0; i--) {
const prevProp = defaultColumns[i].columnProp
const matchedIndex = mergedRows.findIndex(item => item.columnProp === prevProp)
if (matchedIndex > -1) {
insertIndex = matchedIndex + 1
break
}
}
}
mergedRows.splice(insertIndex, 0, Object.assign({}, defaultItem))
usedColumnPropMap[defaultItem.columnProp] = true
})
return mergedRows
},
loadTrackingDefaultColumns (tableId) {
const queryTable = {
functionId: this.getTrackingColumnFunctionId(),
tableId: tableId,
languageCode: this.$i18n && this.$i18n.locale ? this.$i18n.locale : ''
}
return getTableDefaultListLanguage(queryTable).then(({ data }) => {
const rows = data && Array.isArray(data.rows) ? data.rows : []
this.hasUserTrackingColumnConfig = false
this.trackingColumnList = rows.length > 0
? this.normalizeTrackingColumns(rows, true)
: this.buildDefaultTrackingColumns()
this.refreshTrackingTableLayout()
return this.trackingColumnList
}).catch(() => {
this.hasUserTrackingColumnConfig = false
this.trackingColumnList = this.buildDefaultTrackingColumns()
this.refreshTrackingTableLayout()
return this.trackingColumnList
})
},
loadTrackingColumns () {
const tableId = this.getTrackingColumnTableId()
const queryTableUser = {
userId: this.$store.state.user.name,
functionId: this.getTrackingColumnFunctionId(),
tableId: tableId,
status: true,
languageCode: this.$i18n && this.$i18n.locale ? this.$i18n.locale : ''
}
return getTableUserListLanguage(queryTableUser).then(({ data }) => {
const rows = data && Array.isArray(data.rows) ? data.rows : []
if (rows.length > 0) {
this.hasUserTrackingColumnConfig = true
this.trackingColumnList = this.normalizeTrackingColumns(rows, true)
this.refreshTrackingTableLayout()
return this.trackingColumnList
}
return this.loadTrackingDefaultColumns(tableId)
}).catch(() => {
this.hasUserTrackingColumnConfig = false
this.trackingColumnList = this.buildDefaultTrackingColumns()
this.refreshTrackingTableLayout()
return this.trackingColumnList
})
},
isTrackingColumnVisible (column) {
if (!column) {
return false
}
if (this.normalizeTrackingColumnBoolean(column.status, true) === false) {
return false
}
if (this.normalizeTrackingColumnBoolean(column.columnHidden, false) !== false) {
return false
}
// 分类列只服务 RFID;其他 BU 或未选 BU 时即使用户列配置勾选了也不展示。
if (column.columnProp === 'dryWetType' && !this.isRfidBuNo(this.getSelectedSearchBuNo())) {
return false
}
return true
},
// 同步图标只展示在项目编码、项目物料与打样单号三列,避免整列变色影响可读性。
isSyncIconTrackingColumn (columnProp) {
return ['projectNo', 'testPartNo', 'proofingNo'].indexOf(columnProp) > -1
},
// 仅已同步到 NPI 的项目编码/项目物料可跳转,避免临时 tracking 主键误进正式库页面。
canOpenNpiFromTracking (row, columnProp) {
if (!row || this.isBlankValue(this.getTrackingColumnDisplayValue(row, columnProp))) {
return false
}
if (columnProp === 'projectNo') {
return this.isSyncedByIdRule(row.projectId || row.project_id)
}
if (columnProp === 'testPartNo') {
return this.isSyncedByIdRule(this.getTrackingProjectPartId(row))
}
return false
},
openNpiFromTracking (columnProp, row) {
if (!this.canOpenNpiFromTracking(row, columnProp)) {
return
}
if (columnProp === 'projectNo') {
this.openNpiTab('eam-eamProjectInfo', {
projectNo: this.getTrackingColumnDisplayValue(row, 'projectNo')
})
return
}
this.openNpiTab('eam-eamProjectPartInfo', {
projectNo: this.getTrackingColumnDisplayValue(row, 'projectNo'),
testPartNo: this.getTrackingColumnDisplayValue(row, 'testPartNo')
})
},
// 跳转时同步更新已打开页签上的 query,否则再次点页签会带回第一次进入时的旧查询条件。
openNpiTab (routeName, query) {
const tabs = this.$store.state.common.mainTabs || []
const existingTab = tabs.find(item => item && item.name === routeName)
if (existingTab) {
this.$store.commit('common/updateMainTabs', tabs.map(item => {
if (!item || item.name !== routeName) {
return item
}
return Object.assign({}, item, { query: query })
}))
}
this.$router.push({ name: routeName, query: query }).catch((err) => {
if (err && err.name === 'NavigationDuplicated') {
return
}
this.$message.error('未找到对应NPI页面,请确认是否有菜单权限')
})
},
getTrackingColumnDisplayValue (row, columnProp) {
if (!row || !columnProp) {
return ''
}
const value = row[columnProp]
if (columnProp === 'pmInqueryTime') {
return this.formatDate(value)
}
if (columnProp === 'sampleMakingMonth' && !this.isBlankValue(value)) {
const month = Number(value)
if (!Number.isNaN(month)) {
return String(month).padStart(2, '0')
}
}
return this.isBlankValue(value) ? '' : String(value)
},
isDeliveryVarianceAlert (row) {
// Delivery Variance = Baseline - Delivery&Package 实际日期;-8/-9 这类小于 -7 的值表示延误超过 7 天,需要标红。
if (!row || this.isBlankValue(row.deliveryVariance)) {
return false
}
const variance = Number(row.deliveryVariance)
if (Number.isNaN(variance)) {
return false
}
return variance < -7
},
getTrackingRoleDisplayValue (row, nameField, valueField) {
if (!row) {
return ''
}
const roleNameValue = row[nameField]
if (!this.isBlankValue(roleNameValue)) {
return String(roleNameValue)
}
const roleValue = row[valueField]
if (this.isBlankValue(roleValue)) {
return ''
}
const displayName = this.getRoleDisplayName(roleValue)
return this.isBlankValue(displayName) ? '' : displayName
},
getSearchBuOptionList () {
return this.userBuList
.map(item => this.normalizeBuNo(item && item.buNo ? item.buNo : '', this.searchData.site))
.filter(item => !!item)
},
ensureSearchBuNoSelected () {
const buOptionList = this.getSearchBuOptionList()
if (buOptionList.length === 0) {
this.searchData.buNo = ''
return ''
}
const currentBuNo = this.normalizeBuNo(this.searchData.buNo, this.searchData.site)
if (currentBuNo && buOptionList.indexOf(currentBuNo) > -1) {
this.searchData.buNo = currentBuNo
return currentBuNo
}
// 需求要求查询BU必须有值:为空或无效时默认回退到首个可用BU。
this.searchData.buNo = buOptionList[0]
return this.searchData.buNo
},
loadUserBuList () {
if (this.userBuList.length > 0) {
this.ensureSearchBuNoSelected()
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.ensureSearchBuNoSelected()
this.ensureBuProcessConfigRows()
}
return this.userBuList
}).catch(() => {
this.userBuList = []
this.ensureSearchBuNoSelected()
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.mergeProcessCodeLabelMapByRows(this.processColumns)
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()
const processCategoryCode = item.processCategoryCode == null ? '' : String(item.processCategoryCode).trim()
const processCategoryName = item.processCategoryName == null ? '' : String(item.processCategoryName).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,
processCategoryCode: processCategoryCode,
processCategoryName: processCategoryName,
sortNo: sortNo === Number.MAX_SAFE_INTEGER ? null : sortNo
})
})
// 保障切换期可用性:当表里无有效配置时继续使用前端兜底工序定义。
this.processColumns = nextColumns.length > 0 ? nextColumns : cloneDefaultProcessColumns()
this.mergeProcessCodeLabelMapByRows(this.processColumns)
this.syncDataListProcessValues()
this.ensureBuProcessConfigRows()
this.refreshTrackingTableLayout()
return this.processColumns
}).catch(() => {
this.$message.error('加载工序列配置异常,已使用默认配置')
this.processColumns = cloneDefaultProcessColumns()
this.mergeProcessCodeLabelMapByRows(this.processColumns)
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)
if (processCol.code === 'deliveryPackage') {
// Delivery&Package 工序日期与打样预计完成日期使用同一口径,前端先行回写避免用户感知延迟。
this.$set(row, 'requiredDeliveryDate', actualDate || '')
// baseline 仅在首次维护 Delivery&Package 日期时自动带入,后续允许独立维护。
if (!this.formatDate(row.baseline) && actualDate) {
this.$set(row, 'baseline', actualDate)
}
}
},
getAllProcessCodes () {
return this.processColumns.map(item => item.code)
},
mergeProcessCodeLabelMapByRows (rows) {
if (!Array.isArray(rows) || rows.length === 0) {
return
}
const nextMap = Object.assign({}, this.processCodeLabelMap || {})
const rowLabelMap = buildProcessCodeLabelMapByRows(rows)
Object.keys(rowLabelMap).forEach(code => {
nextMap[code] = rowLabelMap[code]
})
this.processCodeLabelMap = nextMap
},
mergeProcessCodeLabelMapByObject (labelMap) {
if (!labelMap || typeof labelMap !== 'object') {
return
}
const nextMap = Object.assign({}, this.processCodeLabelMap || {})
Object.keys(labelMap).forEach(rawCode => {
const code = rawCode == null ? '' : String(rawCode).trim()
const label = labelMap[rawCode] == null ? '' : String(labelMap[rawCode]).trim()
if (!code || !label) {
return
}
// 兼容后端已保存大小写不一致的编码。
nextMap[code] = label
nextMap[code.toLowerCase()] = label
})
this.processCodeLabelMap = nextMap
},
buildProcessCategoryOptionsByRows (rows) {
const sourceRows = Array.isArray(rows) ? rows : []
const nextOptions = []
const usedCodeMap = {}
sourceRows.forEach((item, index) => {
const categoryCode = item && item.categoryCode != null ? String(item.categoryCode).trim() : ''
const categoryName = item && item.categoryName != null ? String(item.categoryName).trim() : ''
if (!categoryCode || !categoryName) {
return
}
const codeKey = categoryCode.toLowerCase()
if (usedCodeMap[codeKey]) {
return
}
usedCodeMap[codeKey] = true
const parsedSortNo = parseInt(item && item.sortNo, 10)
nextOptions.push({
categoryCode: categoryCode,
categoryName: categoryName,
sortNo: Number.isNaN(parsedSortNo) ? (index + 1) * 10 : parsedSortNo
})
})
nextOptions.sort((a, b) => {
if (a.sortNo !== b.sortNo) {
return a.sortNo - b.sortNo
}
return String(a.categoryCode).localeCompare(String(b.categoryCode))
})
return nextOptions
},
getProcessCategoryOptionsByBuNo (buNo) {
const normalizedBuNo = this.normalizeBuNo(buNo, this.$store.state.user.site)
if (!normalizedBuNo) {
return []
}
const optionMap = this.buProcessCategoryOptionsMap || {}
return Array.isArray(optionMap[normalizedBuNo]) ? optionMap[normalizedBuNo] : []
},
getBuProcessCategoryNameByCode (buNo, categoryCode) {
if (!categoryCode) {
return ''
}
const normalizedCategoryCode = String(categoryCode).trim()
if (!normalizedCategoryCode) {
return ''
}
const lowerCategoryCode = normalizedCategoryCode.toLowerCase()
const configEntry = this.getBuProcessConfigEntry(buNo, this.$store.state.user.site)
const categoryNameMap = configEntry && configEntry.config && configEntry.config.processCategoryNameMap
? configEntry.config.processCategoryNameMap
: {}
if (categoryNameMap && typeof categoryNameMap === 'object') {
if (categoryNameMap[normalizedCategoryCode]) {
return categoryNameMap[normalizedCategoryCode]
}
if (categoryNameMap[lowerCategoryCode]) {
return categoryNameMap[lowerCategoryCode]
}
const matchedNameCode = Object.keys(categoryNameMap).find(code => {
return String(code).trim().toLowerCase() === lowerCategoryCode
})
if (matchedNameCode) {
return categoryNameMap[matchedNameCode] || ''
}
}
const options = this.getProcessCategoryOptionsByBuNo(buNo)
const matched = options.find(item => {
if (!item || !item.categoryCode) {
return false
}
return String(item.categoryCode).trim().toLowerCase() === lowerCategoryCode
})
return matched && matched.categoryName ? matched.categoryName : ''
},
getBuProcessCategoryCodeByProcessCode (buNo, processCode) {
const configEntry = this.getBuProcessConfigEntry(buNo, this.$store.state.user.site)
const config = configEntry ? configEntry.config : null
if (!config || !config.processCategoryMap || typeof config.processCategoryMap !== 'object' || !processCode) {
return ''
}
const sourceMap = config.processCategoryMap
const normalizedProcessCode = String(processCode).trim()
if (!normalizedProcessCode) {
return ''
}
if (sourceMap[normalizedProcessCode] != null) {
return String(sourceMap[normalizedProcessCode]).trim()
}
const lowerProcessCode = normalizedProcessCode.toLowerCase()
const matchedKey = Object.keys(sourceMap).find(code => {
return String(code).trim().toLowerCase() === lowerProcessCode
})
return matchedKey ? String(sourceMap[matchedKey] || '').trim() : ''
},
getBuProcessCategoryCode (row, processCode) {
if (!row || !processCode) {
return ''
}
const map = row.processCategoryMap && typeof row.processCategoryMap === 'object'
? row.processCategoryMap
: {}
const directValue = map[processCode]
if (directValue != null) {
return String(directValue).trim()
}
const normalizedCode = String(processCode).trim().toLowerCase()
const matchedKey = Object.keys(map).find(code => String(code).trim().toLowerCase() === normalizedCode)
if (!matchedKey) {
return ''
}
const matchedValue = map[matchedKey]
return matchedValue == null ? '' : String(matchedValue).trim()
},
normalizeProcessCategoryMapForRow (row) {
const resultMap = {}
if (!row) {
return resultMap
}
const validOrderCodes = this.getValidProcessOrderCodes(row.processOrderCodes)
validOrderCodes.forEach(code => {
const categoryCode = this.getBuProcessCategoryCode(row, code)
resultMap[code] = categoryCode || ''
})
return resultMap
},
handleBuProcessCategoryChange (row, processCode, categoryCode) {
if (!row || !processCode) {
return
}
const normalizedCode = String(processCode).trim()
if (!normalizedCode) {
return
}
if (!row.processCategoryMap || typeof row.processCategoryMap !== 'object') {
this.$set(row, 'processCategoryMap', {})
}
const categoryCodeVal = categoryCode == null ? '' : String(categoryCode).trim()
this.$set(row.processCategoryMap, normalizedCode, categoryCodeVal)
},
loadBuProcessCategoryOptionsByBuNo (buNo, silent = true) {
const normalizedBuNo = this.normalizeBuNo(buNo, this.$store.state.user.site)
if (!normalizedBuNo) {
return Promise.resolve([])
}
const inData = {
site: this.$store.state.user.site,
buNo: normalizedBuNo,
userName: this.$store.state.user.name
}
return queryProofTrackingProcessCategories(inData).then(({ data }) => {
if (!(data && data.code === 0)) {
if (!silent) {
this.$message.error((data && data.msg) || `加载事业部【${normalizedBuNo}】工序分类失败`)
}
this.$set(this.buProcessCategoryOptionsMap, normalizedBuNo, [])
if (this.buProcessConfigMap && this.buProcessConfigMap[normalizedBuNo]) {
this.$set(this.buProcessConfigMap, normalizedBuNo, Object.assign({}, this.buProcessConfigMap[normalizedBuNo], {
processCategoryNameMap: {}
}))
}
return []
}
const options = this.buildProcessCategoryOptionsByRows(data.rows || [])
this.$set(this.buProcessCategoryOptionsMap, normalizedBuNo, options)
if (this.buProcessConfigMap && this.buProcessConfigMap[normalizedBuNo]) {
const processCategoryNameMap = {}
options.forEach(item => {
const categoryCode = item && item.categoryCode ? String(item.categoryCode).trim() : ''
const categoryName = item && item.categoryName ? String(item.categoryName).trim() : ''
if (!categoryCode || !categoryName) {
return
}
processCategoryNameMap[categoryCode] = categoryName
processCategoryNameMap[categoryCode.toLowerCase()] = categoryName
})
this.$set(this.buProcessConfigMap, normalizedBuNo, Object.assign({}, this.buProcessConfigMap[normalizedBuNo], {
processCategoryNameMap: processCategoryNameMap
}))
}
return options
}).catch(() => {
if (!silent) {
this.$message.error(`加载事业部【${normalizedBuNo}】工序分类异常`)
}
this.$set(this.buProcessCategoryOptionsMap, normalizedBuNo, [])
if (this.buProcessConfigMap && this.buProcessConfigMap[normalizedBuNo]) {
this.$set(this.buProcessConfigMap, normalizedBuNo, Object.assign({}, this.buProcessConfigMap[normalizedBuNo], {
processCategoryNameMap: {}
}))
}
return []
})
},
loadAllBuProcessCategoryOptions (sourceRows, silent = true) {
const rows = Array.isArray(sourceRows) ? sourceRows : []
const requestBuNos = []
const usedBuMap = {}
rows.forEach(item => {
const buNo = this.normalizeBuNo(item && item.buNo ? item.buNo : '', this.$store.state.user.site)
if (!buNo || usedBuMap[buNo]) {
return
}
usedBuMap[buNo] = true
requestBuNos.push(buNo)
})
if (requestBuNos.length === 0) {
return Promise.resolve([])
}
return Promise.all(requestBuNos.map(buNo => this.loadBuProcessCategoryOptionsByBuNo(buNo, silent)))
},
getProcessLabelByCode (processCode) {
if (!processCode) {
return ''
}
const normalizedCode = String(processCode).trim()
const lowerCode = normalizedCode.toLowerCase()
const matched = this.processColumns.find(item => item && item.code === normalizedCode)
if (matched && matched.label) {
return matched.label
}
const caseInsensitiveMatched = this.processColumns.find(item => {
if (!item || !item.code) {
return false
}
return String(item.code).trim().toLowerCase() === lowerCode
})
if (caseInsensitiveMatched && caseInsensitiveMatched.label) {
return caseInsensitiveMatched.label
}
const codeLabelMap = this.processCodeLabelMap || {}
if (codeLabelMap[normalizedCode]) {
return codeLabelMap[normalizedCode]
}
if (codeLabelMap[lowerCode]) {
return codeLabelMap[lowerCode]
}
const defaultMatched = DEFAULT_PROCESS_COLUMNS.find(item => {
if (!item || !item.code) {
return false
}
return item.code.toLowerCase() === lowerCode
})
return defaultMatched && defaultMatched.label ? defaultMatched.label : normalizedCode
},
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)
},
isRfidBuNo (buNo) {
const normalizedBuNo = this.normalizeBuNo(buNo, this.$store.state.user.site)
return normalizedBuNo.toUpperCase() === '03-RFID'
},
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)
},
normalizeSearchProofingStatusList (statusList) {
if (!Array.isArray(statusList) || statusList.length === 0) {
return []
}
const normalizedStatusList = []
statusList.forEach(item => {
const status = this.isBlankValue(item) ? '' : String(item).trim()
if (!status || normalizedStatusList.indexOf(status) > -1) {
return
}
normalizedStatusList.push(status)
})
return normalizedStatusList
},
getSearchCustomerOptionLabel (item) {
const customerNo = item && !this.isBlankValue(item.customerNo)
? String(item.customerNo).trim()
: ''
const customerDesc = item && !this.isBlankValue(item.customerDesc)
? String(item.customerDesc).trim()
: ''
if (!customerNo) {
return customerDesc
}
return customerDesc
},
resetSearchCustomerOptions () {
// 查询 BU 变化后,客户下拉需重置,避免沿用旧 BU 缓存导致筛选项不准确。
this.searchCustomerQueryToken += 1
this.searchCustomerOptionLoading = false
this.searchCustomerOptionInitialized = false
this.searchCustomerOptions = []
},
handleSearchCustomerVisibleChange (visible) {
if (!visible || this.searchCustomerOptionInitialized) {
return
}
this.querySearchCustomerOptions('')
},
handleSearchCustomerRemote (keyword) {
this.querySearchCustomerOptions(keyword)
},
querySearchCustomerOptions (keyword) {
const normalizedKeyword = this.isBlankValue(keyword) ? '' : String(keyword).trim()
const queryToken = this.searchCustomerQueryToken + 1
this.searchCustomerQueryToken = queryToken
this.searchCustomerOptionLoading = true
return queryProofTrackingCustomerOptions({
site: this.$store.state.user.site,
userName: this.$store.state.user.name,
buNo: this.getSelectedSearchBuNo(),
customerNo: normalizedKeyword
}).then(({ data }) => {
if (queryToken !== this.searchCustomerQueryToken) {
return []
}
if (!(data && data.code === 0)) {
throw new Error((data && data.msg) || '查询客户下拉异常')
}
const rows = Array.isArray(data.rows) ? data.rows : []
const normalizedRows = rows.map(item => ({
customerNo: this.isBlankValue(item && item.customerNo) ? '' : String(item.customerNo).trim(),
customerDesc: this.isBlankValue(item && item.customerDesc) ? '' : String(item.customerDesc).trim()
})).filter(item => !!item.customerNo)
this.searchCustomerOptions = normalizedRows
if (!normalizedKeyword) {
this.searchCustomerOptionInitialized = true
}
return normalizedRows
}).catch((e) => {
if (queryToken === this.searchCustomerQueryToken) {
this.searchCustomerOptions = []
this.$message.error((e && e.message) || '查询客户下拉异常')
}
return []
}).finally(() => {
if (queryToken === this.searchCustomerQueryToken) {
this.searchCustomerOptionLoading = false
}
})
},
handleSearchBuChange () {
this.ensureSearchBuNoSelected()
this.resetSearchCustomerOptions()
// 分类列仅 RFID 可见,切走时清空避免隐藏条件继续过滤。
if (!this.isRfidBuNo(this.searchData.buNo)) {
this.searchData.dryWetType = ''
}
this.refreshTrackingTableLayout()
// 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
},
getProcessColumnWidth (item) {
const rows = this.dataList.filter(row =>
this.isProcessColumnVisibleForRow(row, item)
)
// 没有数据
if (rows.length === 0) {
return 110
}
// 有任何一个未完成
if (rows.some(row => !this.isProcessStatusComplete(row, item))) {
return 134
}
// 全部完成
return 110
},
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
}
return queryProofTrackingBuProcessConfig(inData).then(({ data }) => {
if (!(data && data.code === 0)) {
this.$message.error((data && data.msg) || '加载事业部进度展示配置失败')
return
}
const rows = data.rows || []
const nextMap = {}
const processLabelMap = {}
rows.forEach(item => {
if (item && item.processLabelMap && typeof item.processLabelMap === 'object') {
Object.keys(item.processLabelMap).forEach(rawCode => {
const code = rawCode == null ? '' : String(rawCode).trim()
const label = item.processLabelMap[rawCode] == null ? '' : String(item.processLabelMap[rawCode]).trim()
if (!code || !label) {
return
}
processLabelMap[code] = label
})
}
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
})
const processCategoryMap = {}
const sourceCategoryMap = item && item.processCategoryMap && typeof item.processCategoryMap === 'object'
? item.processCategoryMap
: {}
const processCategoryNameMap = {}
const sourceCategoryNameMap = item && item.processCategoryNameMap && typeof item.processCategoryNameMap === 'object'
? item.processCategoryNameMap
: {}
Object.keys(sourceCategoryNameMap).forEach(rawCategoryCode => {
const categoryCode = rawCategoryCode == null ? '' : String(rawCategoryCode).trim()
const categoryName = sourceCategoryNameMap[rawCategoryCode] == null
? ''
: String(sourceCategoryNameMap[rawCategoryCode]).trim()
if (!categoryCode || !categoryName) {
return
}
processCategoryNameMap[categoryCode] = categoryName
processCategoryNameMap[categoryCode.toLowerCase()] = categoryName
})
processOrderCodes.forEach(code => {
if (!code) {
return
}
const directVal = sourceCategoryMap[code]
if (directVal != null) {
processCategoryMap[code] = String(directVal).trim()
return
}
const lowerCode = String(code).trim().toLowerCase()
const matchedKey = Object.keys(sourceCategoryMap).find(rawCode => {
return String(rawCode).trim().toLowerCase() === lowerCode
})
processCategoryMap[code] = matchedKey ? String(sourceCategoryMap[matchedKey] || '').trim() : ''
})
nextMap[buNo] = {
showAll: showAll,
processOrderCodes: processOrderCodes,
processCodes: showAll ? processOrderCodes.slice() : processOrderCodes.filter(code => selectedCodeMap[code]),
processCategoryMap: processCategoryMap,
processCategoryNameMap: processCategoryNameMap
}
})
this.mergeProcessCodeLabelMapByObject(processLabelMap)
this.buProcessConfigMap = nextMap
this.ensureBuProcessConfigRows()
this.loadAllBuProcessCategoryOptions(this.buProcessConfigRows, true)
this.refreshTrackingTableLayout()
}).catch(() => {
this.$message.error('加载事业部进度展示配置异常')
})
},
ensureBuProcessConfigRows () {
const allCodes = this.getAllProcessCodes()
const builtinCodeMap = {}
DEFAULT_PROCESS_COLUMNS.forEach(item => {
if (item && item.code) {
builtinCodeMap[item.code] = true
}
})
const defaultSelectedCodes = allCodes.filter(code => !!builtinCodeMap[code])
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: (defaultSelectedCodes.length > 0 ? defaultSelectedCodes : allCodes).slice(),
processCategoryMap: {}
}
if (cfg) {
const validOrderCodes = this.getValidProcessOrderCodes(cfg.processOrderCodes)
row.processOrderCodes = validOrderCodes
const validCodes = this.getValidProcessCodes(cfg.processCodes, validOrderCodes)
if (validCodes.length > 0) {
const selectedCodeMap = {}
validCodes.forEach(code => {
selectedCodeMap[code] = true
})
row.processCodes = validOrderCodes.filter(code => selectedCodeMap[code])
} else {
const defaultSelectedCodeMap = {}
row.processCodes.forEach(code => {
defaultSelectedCodeMap[code] = true
})
row.processCodes = validOrderCodes.filter(code => defaultSelectedCodeMap[code])
}
row.processCategoryMap = cfg.processCategoryMap && typeof cfg.processCategoryMap === 'object'
? Object.assign({}, cfg.processCategoryMap)
: {}
}
row.processCategoryMap = this.normalizeProcessCategoryMapForRow(row)
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 () {
if (this.buProcessConfigLoading) {
return
}
this.buProcessConfigLoading = true
this.processOrderDragState = {
buNo: '',
fromIndex: -1
}
// 每次打开都强制拉取后端最新配置,避免必须整页刷新才能看到变更。
Promise.all([
this.queryProcessColumns(),
this.queryBuProcessConfig()
]).finally(() => {
this.ensureBuProcessConfigRows()
this.buProcessConfigDialogVisible = true
this.loadAllBuProcessCategoryOptions(this.buProcessConfigRows, false).finally(() => {
this.buProcessConfigLoading = false
})
})
},
buildDefaultProcessConfigRow (item) {
const parsedSortNo = parseInt(item && item.sortNo, 10)
const activeFlag = item && item.activeFlag ? String(item.activeFlag).trim().toUpperCase() : 'Y'
const code = item && item.code ? String(item.code).trim() : ''
return {
_rowKey: `default_process_${Date.now()}_${this.defaultProcessConfigRowSeed++}`,
// 记录后端原始编码,删除时可避免“修改了编码但尚未保存”导致删不到原记录。
_persistedCode: code,
sortNo: Number.isNaN(parsedSortNo) || parsedSortNo <= 0 ? this.getNextDefaultProcessSortNo() : parsedSortNo,
code: code,
label: item && item.label ? String(item.label).trim() : '',
activeFlag: activeFlag === 'N' ? 'N' : 'Y'
}
},
getNextDefaultProcessSortNo () {
if (!Array.isArray(this.defaultProcessConfigRows) || this.defaultProcessConfigRows.length === 0) {
return 10
}
let maxSortNo = 0
this.defaultProcessConfigRows.forEach(row => {
const parsedSortNo = parseInt(row && row.sortNo, 10)
if (!Number.isNaN(parsedSortNo) && parsedSortNo > maxSortNo) {
maxSortNo = parsedSortNo
}
})
return maxSortNo > 0 ? maxSortNo + 10 : (this.defaultProcessConfigRows.length + 1) * 10
},
resetDefaultProcessConfigSortNoByOrder () {
if (!Array.isArray(this.defaultProcessConfigRows)) {
return
}
this.defaultProcessConfigRows.forEach((row, index) => {
if (!row) {
return
}
this.$set(row, 'sortNo', (index + 1) * 10)
})
},
clearDefaultProcessConfigSelection () {
this.defaultProcessConfigSelectionRows = []
this.$nextTick(() => {
const tableRef = this.$refs.defaultProcessConfigTable
if (tableRef && tableRef.clearSelection) {
tableRef.clearSelection()
}
})
},
destroyDefaultProcessConfigSortable () {
if (this.defaultProcessConfigSortable && typeof this.defaultProcessConfigSortable.destroy === 'function') {
this.defaultProcessConfigSortable.destroy()
}
this.defaultProcessConfigSortable = null
},
initDefaultProcessConfigSortable () {
this.$nextTick(() => {
this.destroyDefaultProcessConfigSortable()
if (!this.defaultProcessConfigDialogVisible || !Array.isArray(this.defaultProcessConfigRows) || this.defaultProcessConfigRows.length === 0) {
return
}
const tableRef = this.$refs.defaultProcessConfigTable
const tableBody = tableRef && tableRef.$el
? tableRef.$el.querySelector('.el-table__body-wrapper tbody')
: null
if (!tableBody) {
return
}
this.defaultProcessConfigSortable = Sortable.create(tableBody, {
animation: 150,
// 整行可拖拽,避免必须精确悬浮到小图标才能排序。
handle: 'tr',
draggable: 'tr',
ghostClass: 'default-process-sortable-ghost',
chosenClass: 'default-process-sortable-chosen',
dragClass: 'default-process-sortable-drag',
onEnd: (evt) => {
const oldIndex = evt && typeof evt.oldIndex === 'number' ? evt.oldIndex : -1
const newIndex = evt && typeof evt.newIndex === 'number' ? evt.newIndex : -1
if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) {
return
}
const movedRow = this.defaultProcessConfigRows.splice(oldIndex, 1)[0]
this.defaultProcessConfigRows.splice(newIndex, 0, movedRow)
this.resetDefaultProcessConfigSortNoByOrder()
this.clearDefaultProcessConfigSelection()
}
})
})
},
handleDefaultProcessConfigDialogClosed () {
this.defaultProcessConfigDeleteLoading = false
this.clearDefaultProcessConfigSelection()
this.destroyDefaultProcessConfigSortable()
},
handleDefaultProcessConfigSelectionChange (rows) {
this.defaultProcessConfigSelectionRows = Array.isArray(rows) ? rows : []
},
normalizeDefaultProcessFieldBaseByCode (code) {
const rawCode = code == null ? '' : String(code).trim()
if (!rawCode) {
return ''
}
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(rawCode)) {
return rawCode
}
const parts = rawCode.split(/[^A-Za-z0-9]+/).filter(item => !!item)
if (parts.length === 0) {
return ''
}
const firstPart = parts[0].toLowerCase()
const remained = parts.slice(1).map(item => {
const lower = item.toLowerCase()
return `${lower.charAt(0).toUpperCase()}${lower.substring(1)}`
}).join('')
let fieldBase = `${firstPart}${remained}`
if (/^[0-9]/.test(fieldBase)) {
fieldBase = `p${fieldBase}`
}
return fieldBase
},
buildDefaultProcessFieldsByCode (code) {
const fieldBase = this.normalizeDefaultProcessFieldBaseByCode(code)
if (!fieldBase) {
return {
planField: '',
actualField: '',
statusField: ''
}
}
return {
planField: `${fieldBase}PlanDate`,
actualField: `${fieldBase}ActualDate`,
statusField: `${fieldBase}Status`
}
},
queryDefaultProcessConfig () {
const inData = {
site: '*',
buNo: '*',
userName: this.$store.state.user.name
}
return queryProofTrackingProcessColumnsManage(inData).then(({ data }) => {
if (!(data && data.code === 0)) {
this.$message.error((data && data.msg) || '加载默认工序配置失败')
this.defaultProcessConfigRows = []
this.defaultProcessConfigSelectionRows = []
return []
}
const rows = Array.isArray(data.rows) ? data.rows : []
const sortedRows = rows
.map((item, index) => {
const parsedSortNo = parseInt(item && item.sortNo, 10)
return {
sourceIndex: index,
sortNo: Number.isNaN(parsedSortNo) ? Number.MAX_SAFE_INTEGER : parsedSortNo,
item: item
}
})
.sort((a, b) => {
if (a.sortNo !== b.sortNo) {
return a.sortNo - b.sortNo
}
return a.sourceIndex - b.sourceIndex
})
this.defaultProcessConfigRows = sortedRows.map(({ item }) => this.buildDefaultProcessConfigRow(item))
this.resetDefaultProcessConfigSortNoByOrder()
this.clearDefaultProcessConfigSelection()
this.initDefaultProcessConfigSortable()
return this.defaultProcessConfigRows
}).catch(() => {
this.$message.error('加载默认工序配置异常')
this.defaultProcessConfigRows = []
this.defaultProcessConfigSelectionRows = []
this.destroyDefaultProcessConfigSortable()
return []
})
},
openDefaultProcessConfigDialog () {
this.defaultProcessConfigDeleteLoading = false
this.defaultProcessConfigDialogVisible = true
this.queryDefaultProcessConfig()
},
buildProcessCategoryRow (item) {
const parsedSortNo = parseInt(item && item.sortNo, 10)
const categoryCode = item && item.categoryCode ? String(item.categoryCode).trim() : ''
return {
_rowKey: `process_category_${Date.now()}_${this.processCategoryRowSeed++}`,
_persistedCode: categoryCode,
sortNo: Number.isNaN(parsedSortNo) || parsedSortNo <= 0 ? (this.processCategoryRows.length + 1) * 10 : parsedSortNo,
categoryCode: categoryCode,
categoryName: item && item.categoryName ? String(item.categoryName).trim() : ''
}
},
queryProcessCategoryConfig (buNo, silent = false) {
const normalizedBuNo = this.normalizeBuNo(buNo, this.$store.state.user.site)
if (!normalizedBuNo) {
return Promise.resolve([])
}
const inData = {
site: this.$store.state.user.site,
buNo: normalizedBuNo,
userName: this.$store.state.user.name
}
return queryProofTrackingProcessCategories(inData).then(({ data }) => {
if (!(data && data.code === 0)) {
if (!silent) {
this.$message.error((data && data.msg) || '加载工序分类失败')
}
this.processCategoryRows = []
this.processCategorySelectionRows = []
this.$set(this.buProcessCategoryOptionsMap, normalizedBuNo, [])
if (this.buProcessConfigMap && this.buProcessConfigMap[normalizedBuNo]) {
this.$set(this.buProcessConfigMap, normalizedBuNo, Object.assign({}, this.buProcessConfigMap[normalizedBuNo], {
processCategoryNameMap: {}
}))
}
return []
}
const rows = Array.isArray(data.rows) ? data.rows : []
const categoryOptions = this.buildProcessCategoryOptionsByRows(rows)
this.$set(this.buProcessCategoryOptionsMap, normalizedBuNo, categoryOptions)
if (this.buProcessConfigMap && this.buProcessConfigMap[normalizedBuNo]) {
const processCategoryNameMap = {}
categoryOptions.forEach(item => {
const categoryCode = item && item.categoryCode ? String(item.categoryCode).trim() : ''
const categoryName = item && item.categoryName ? String(item.categoryName).trim() : ''
if (!categoryCode || !categoryName) {
return
}
processCategoryNameMap[categoryCode] = categoryName
processCategoryNameMap[categoryCode.toLowerCase()] = categoryName
})
this.$set(this.buProcessConfigMap, normalizedBuNo, Object.assign({}, this.buProcessConfigMap[normalizedBuNo], {
processCategoryNameMap: processCategoryNameMap
}))
}
const sortedRows = rows
.map((item, index) => {
const parsedSortNo = parseInt(item && item.sortNo, 10)
return {
sourceIndex: index,
sortNo: Number.isNaN(parsedSortNo) ? Number.MAX_SAFE_INTEGER : parsedSortNo,
item: item
}
})
.sort((a, b) => {
if (a.sortNo !== b.sortNo) {
return a.sortNo - b.sortNo
}
return a.sourceIndex - b.sourceIndex
})
this.processCategoryRows = sortedRows.map(({ item }) => this.buildProcessCategoryRow(item))
this.processCategorySelectionRows = []
return this.processCategoryRows
}).catch(() => {
if (!silent) {
this.$message.error('加载工序分类异常')
}
this.processCategoryRows = []
this.processCategorySelectionRows = []
this.$set(this.buProcessCategoryOptionsMap, normalizedBuNo, [])
if (this.buProcessConfigMap && this.buProcessConfigMap[normalizedBuNo]) {
this.$set(this.buProcessConfigMap, normalizedBuNo, Object.assign({}, this.buProcessConfigMap[normalizedBuNo], {
processCategoryNameMap: {}
}))
}
return []
})
},
openProcessCategoryDialog (row) {
const buNo = this.normalizeBuNo(row && row.buNo ? row.buNo : '', this.$store.state.user.site)
if (!buNo) {
this.$message.warning('请在事业部进度设置中指定事业部后再维护分类')
return
}
this.processCategoryDialogBuNo = buNo
this.processCategoryDialogBuDesc = row && row.buDesc ? row.buDesc : buNo
this.processCategoryDialogVisible = true
this.processCategorySaveLoading = false
this.queryProcessCategoryConfig(buNo)
},
handleProcessCategoryDialogClosed () {
this.processCategoryRows = []
this.processCategorySelectionRows = []
this.processCategoryDialogBuNo = ''
this.processCategoryDialogBuDesc = ''
this.processCategorySaveLoading = false
},
handleProcessCategorySelectionChange (rows) {
this.processCategorySelectionRows = Array.isArray(rows) ? rows : []
},
addProcessCategoryRow () {
this.processCategoryRows.push(this.buildProcessCategoryRow({
sortNo: (this.processCategoryRows.length + 1) * 10
}))
},
deleteProcessCategoryRows () {
if (!Array.isArray(this.processCategorySelectionRows) || this.processCategorySelectionRows.length === 0) {
this.$message.warning('请先勾选要删除的分类')
return
}
const selectedRows = this.processCategorySelectionRows.slice()
this.$confirm(`确认删除选中的 ${selectedRows.length} 条分类吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const selectedKeyMap = {}
selectedRows.forEach(item => {
if (item && item._rowKey) {
selectedKeyMap[item._rowKey] = true
}
})
const nextRows = this.processCategoryRows.filter(item => !(item && selectedKeyMap[item._rowKey]))
const submitRows = this.normalizeProcessCategoryRowsForSave(nextRows)
if (!submitRows) {
return
}
const inData = {
site: this.$store.state.user.site,
buNo: this.processCategoryDialogBuNo,
updateBy: this.$store.state.user.name,
rows: submitRows
}
this.processCategorySaveLoading = true
saveProofTrackingProcessCategoriesManage(inData).then(({ data }) => {
if (!(data && data.code === 0)) {
this.$message.error((data && data.msg) || '删除工序分类失败')
return
}
this.handleProcessCategoryConfigSaved(data.msg || '删除成功')
}).catch(() => {
this.$message.error('删除工序分类异常')
}).finally(() => {
this.processCategorySaveLoading = false
})
}).catch(() => {
// 用户取消删除时无需额外提示。
})
},
normalizeProcessCategoryRowsForSave (sourceRowsInput) {
const sourceRows = Array.isArray(sourceRowsInput)
? sourceRowsInput
: (Array.isArray(this.processCategoryRows) ? this.processCategoryRows : [])
const normalizedRows = []
const usedNameMap = {}
for (let i = 0; i < sourceRows.length; i++) {
const row = sourceRows[i] || {}
const rowNo = i + 1
const categoryCode = row.categoryCode == null ? '' : String(row.categoryCode).trim()
const categoryName = row.categoryName == null ? '' : String(row.categoryName).trim()
if (!categoryName) {
this.$message.warning(`${rowNo}行的分类名称不能为空`)
return null
}
const nameKey = categoryName.toLowerCase()
if (usedNameMap[nameKey]) {
this.$message.warning(`分类名称【${categoryName}】重复,请检查`)
return null
}
usedNameMap[nameKey] = true
normalizedRows.push({
// 旧分类携带隐藏编码回传,便于后端识别“保留/新增/删除”并做使用校验。
categoryCode: categoryCode || null,
categoryName: categoryName,
sortNo: rowNo * 10
})
}
return normalizedRows
},
handleProcessCategoryConfigSaved (successMsg) {
this.$message.success(successMsg || '工序分类已保存')
this.queryProcessCategoryConfig(this.processCategoryDialogBuNo, true).then(() => {
const buNo = this.processCategoryDialogBuNo
const validCategoryCodeMap = {}
this.getProcessCategoryOptionsByBuNo(buNo).forEach(item => {
if (item && item.categoryCode) {
validCategoryCodeMap[String(item.categoryCode).trim()] = true
}
})
this.buProcessConfigRows.forEach(configRow => {
const rowBuNo = this.normalizeBuNo(configRow && configRow.buNo ? configRow.buNo : '', this.$store.state.user.site)
if (!rowBuNo || rowBuNo !== buNo) {
return
}
const nextCategoryMap = this.normalizeProcessCategoryMapForRow(configRow)
Object.keys(nextCategoryMap).forEach(processCode => {
const categoryCode = nextCategoryMap[processCode]
if (!categoryCode || validCategoryCodeMap[categoryCode]) {
return
}
nextCategoryMap[processCode] = ''
})
this.$set(configRow, 'processCategoryMap', nextCategoryMap)
})
this.queryProcessColumns().then(() => {
this.refreshTrackingTableLayout()
})
})
},
saveProcessCategoryConfig () {
const submitRows = this.normalizeProcessCategoryRowsForSave()
if (!submitRows) {
return
}
const inData = {
site: this.$store.state.user.site,
buNo: this.processCategoryDialogBuNo,
updateBy: this.$store.state.user.name,
rows: submitRows
}
this.processCategorySaveLoading = true
saveProofTrackingProcessCategoriesManage(inData).then(({ data }) => {
if (!(data && data.code === 0)) {
this.$message.error((data && data.msg) || '工序分类保存失败')
return
}
this.handleProcessCategoryConfigSaved(data.msg || '工序分类已保存')
}).catch(() => {
this.$message.error('工序分类保存异常')
}).finally(() => {
this.processCategorySaveLoading = false
})
},
addDefaultProcessConfigRow () {
this.defaultProcessConfigRows.push(this.buildDefaultProcessConfigRow({
sortNo: this.getNextDefaultProcessSortNo(),
activeFlag: 'Y'
}))
this.resetDefaultProcessConfigSortNoByOrder()
this.initDefaultProcessConfigSortable()
},
deleteDefaultProcessConfigRow () {
if (!this.defaultProcessConfigSelectionRows || this.defaultProcessConfigSelectionRows.length === 0) {
this.$message.warning('请先勾选要删除的行')
return
}
const selectedRows = this.defaultProcessConfigSelectionRows.slice()
this.$confirm(`确认删除选中的 ${selectedRows.length} 条默认工序吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const selectedKeyMap = {}
const persistedCodeMap = {}
selectedRows.forEach(item => {
if (item && item._rowKey) {
selectedKeyMap[item._rowKey] = true
}
const persistedCode = item && item._persistedCode ? String(item._persistedCode).trim() : ''
const persistedCodeKey = persistedCode.toLowerCase()
if (persistedCode && !persistedCodeMap[persistedCodeKey]) {
persistedCodeMap[persistedCodeKey] = persistedCode
}
})
const nextRows = this.defaultProcessConfigRows.filter(row => {
return !(row && row._rowKey && selectedKeyMap[row._rowKey])
})
const applyDeletedRowsToUi = () => {
this.defaultProcessConfigRows = nextRows
this.resetDefaultProcessConfigSortNoByOrder()
this.clearDefaultProcessConfigSelection()
this.initDefaultProcessConfigSortable()
}
const persistedCodes = Object.keys(persistedCodeMap).map(key => persistedCodeMap[key])
if (persistedCodes.length === 0) {
applyDeletedRowsToUi()
this.$message.success('删除成功')
return
}
const inData = {
site: '*',
buNo: '*',
updateBy: this.$store.state.user.name,
processCodes: persistedCodes
}
this.defaultProcessConfigDeleteLoading = true
deleteProofTrackingProcessColumnsManage(inData).then(({ data }) => {
if (!(data && data.code === 0)) {
this.$message.error((data && data.msg) || '删除默认工序失败')
return
}
applyDeletedRowsToUi()
this.$message.success(data.msg || '删除成功')
// 删除动作实时落库后立即刷新主列表列定义,避免页面继续展示已删工序。
this.queryProcessColumns().then(() => {
this.queryBuProcessConfig()
this.getDataList()
this.refreshTrackingTableLayout()
})
}).catch(() => {
this.$message.error('删除默认工序异常')
}).finally(() => {
this.defaultProcessConfigDeleteLoading = false
})
}).catch(() => {
// 用户取消删除时无需额外提示。
})
},
normalizeDefaultProcessConfigRowsForSave () {
const sourceRows = Array.isArray(this.defaultProcessConfigRows) ? this.defaultProcessConfigRows : []
const normalizedRows = []
const usedCodeMap = {}
for (let i = 0; i < sourceRows.length; i++) {
const row = sourceRows[i] || {}
const rowNo = i + 1
const code = row.code == null ? '' : String(row.code).trim()
const codeKey = code.toLowerCase()
const label = row.label == null ? '' : String(row.label).trim()
if (!code) {
this.$message.warning(`${rowNo}行的工序编码不能为空`)
return null
}
if (usedCodeMap[codeKey]) {
this.$message.warning(`工序编码【${code}】重复,请检查`)
return null
}
if (!label) {
this.$message.warning(`${rowNo}行的工序名称不能为空`)
return null
}
const fieldData = this.buildDefaultProcessFieldsByCode(code)
if (!fieldData.planField || !fieldData.actualField || !fieldData.statusField) {
this.$message.warning(`${rowNo}行的工序编码格式无法生成字段,请调整编码`)
return null
}
usedCodeMap[codeKey] = true
normalizedRows.push({
code: code,
label: label,
planField: fieldData.planField,
actualField: fieldData.actualField,
statusField: fieldData.statusField,
// 用户要求:默认进度设置不维护工序分类。
processCategoryCode: null,
sortNo: rowNo * 10,
activeFlag: String(row.activeFlag || 'Y').toUpperCase() === 'N' ? 'N' : 'Y'
})
}
return normalizedRows
},
saveDefaultProcessConfig () {
const submitRows = this.normalizeDefaultProcessConfigRowsForSave()
if (!submitRows) {
return
}
const executeSave = () => {
const inData = {
site: '*',
buNo: '*',
updateBy: this.$store.state.user.name,
rows: submitRows
}
saveProofTrackingProcessColumnsManage(inData).then(({ data }) => {
if (!(data && data.code === 0)) {
this.$message.error((data && data.msg) || '默认工序配置保存失败')
return
}
this.$message.success(data.msg || '默认工序配置已保存')
this.queryDefaultProcessConfig()
// 保存后刷新页面列定义,确保新增/删除工序立即反映到主表列。
this.queryProcessColumns().then(() => {
this.queryBuProcessConfig()
this.getDataList()
this.refreshTrackingTableLayout()
})
}).catch(() => {
this.$message.error('默认工序配置保存异常')
})
}
if (submitRows.length === 0) {
this.$confirm('当前将清空默认工序配置,保存后页面会回退到系统兜底配置,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
executeSave()
}).catch(() => {
// 用户取消清空时无需额外提示。
})
return
}
executeSave()
},
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])
row.processCategoryMap = this.normalizeProcessCategoryMapForRow(row)
},
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 processCategoryMap = this.normalizeProcessCategoryMapForRow(Object.assign({}, row, {
processOrderCodes: validOrderCodes
}))
const processCategoryNameMap = {}
this.getProcessCategoryOptionsByBuNo(buNo).forEach(item => {
const categoryCode = item && item.categoryCode ? String(item.categoryCode).trim() : ''
const categoryName = item && item.categoryName ? String(item.categoryName).trim() : ''
if (!categoryCode || !categoryName) {
return
}
processCategoryNameMap[categoryCode] = categoryName
processCategoryNameMap[categoryCode.toLowerCase()] = categoryName
})
const showAll = validCodes.length >= validOrderCodes.length
nextMap[buNo] = {
showAll: showAll,
processOrderCodes: validOrderCodes,
processCodes: showAll ? validOrderCodes.slice() : validCodes,
processCategoryMap: processCategoryMap,
processCategoryNameMap: processCategoryNameMap
}
submitRows.push({
buNo: buNo,
showAll: showAll,
processCodes: validCodes,
processOrderCodes: validOrderCodes,
processCategoryMap: processCategoryMap
})
}
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.$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: queryData && queryData.limit ? queryData.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, '')
})
},
getRoleFieldByTagNo (tagNo) {
const numericTag = Number(tagNo)
const fieldKeys = Object.keys(this.roleConfig || {})
for (let i = 0; i < fieldKeys.length; i++) {
const fieldKey = fieldKeys[i]
if (Number(this.roleConfig[fieldKey].tagNo) === numericTag) {
return fieldKey
}
}
return ''
},
buildRoleValueFromChooserRow (row) {
if (!row) {
return ''
}
const username = row.username || row.user_name || ''
const userDisplay = row.user_display || row.userDisplay || ''
if (!username && !userDisplay) {
return ''
}
return username + '-' + userDisplay
},
getBaseData (val) {
if (this.tagNo === 509) {
this.oneKeyForm.customerNo = val.customer_no
this.oneKeyForm.customerDesc = val.customer_desc
return
}
// IQC/SQE/FQC2/IPQC-Hardtag/前道工程师与 NPI 项目页共用 Chooselist 回填规则。
const roleField = this.getRoleFieldByTagNo(this.tagNo)
if (!roleField) {
return
}
this.setOneKeyRoleFieldValue(roleField, this.buildRoleValueFromChooserRow(val))
},
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
}
if (roleCfg.tagNo) {
this.getBaseList(roleCfg.tagNo)
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) {
if (!this.roleDialogField) {
return
}
this.setOneKeyRoleFieldValue(this.roleDialogField, this.buildRoleValueFromChooserRow(row))
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: '',
dry_wet_type: '',
partStatus: '草稿',
projectPhase: '',
tracker: '',
engineer: '',
engineerName: '',
priorityLevel: '',
proofingNo: '',
proofingStatus: '草稿',
proofingNumber: '',
projectCreationDate: '',
projectCloseDate: '',
buildDate: '',
closeDate: '',
comments: '',
planStartDate: '',
requiredDeliveryDate: '',
baseline: '',
pmInqueryTime: '',
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: '',
pmInqueryTime: '',
proofingStatus: '草稿',
remark: '',
createBy: this.$store.state.user.name,
updateBy: this.$store.state.user.name
}
},
normalizeSearchInteger (value) {
if (this.isBlankValue(value)) {
return null
}
const parsedValue = parseInt(value, 10)
return Number.isNaN(parsedValue) ? null : parsedValue
},
applyProofTrackingSearchFilters (inData) {
if (!inData) {
return inData
}
// 年/月筛选对应打样完成日期;空值转 null,避免空字符串把 Integer 查询参数打坏。
inData.sampleQty = this.normalizeSearchInteger(inData.sampleQty)
inData.sampleMakingYear = this.normalizeSearchInteger(inData.sampleMakingYear)
inData.sampleMakingMonth = this.normalizeSearchInteger(inData.sampleMakingMonth)
inData.pmInqueryTimeStart = this.isBlankValue(inData.pmInqueryTimeStart) ? null : inData.pmInqueryTimeStart
inData.pmInqueryTimeEnd = this.isBlankValue(inData.pmInqueryTimeEnd) ? null : inData.pmInqueryTimeEnd
// 非 RFID 查询不带分类条件,避免筛选项隐藏后仍按旧值过滤。
if (!this.isRfidBuNo(inData.buNo || this.searchData.buNo)) {
inData.dryWetType = ''
}
return inData
},
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 normalizedProofingStatusList = this.normalizeSearchProofingStatusList(this.searchData.proofingStatusList)
this.searchData.proofingStatusList = normalizedProofingStatusList
const inData = Object.assign({}, this.searchData, {
page: this.pageIndex,
limit: this.pageSize
})
inData.buNo = this.normalizeBuNo(inData.buNo, inData.site)
inData.proofingStatusList = normalizedProofingStatusList
this.applyProofTrackingSearchFilters(inData)
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.resetDataListPageOrderIndexes()
this.applyTrackingTableSort()
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('查询异常')
})
},
async exportTrackingData () {
if (this.exportLoading) {
return
}
this.searchData.site = this.$store.state.user.site
this.searchData.userName = this.$store.state.user.name
const exportTrackingColumns = this.visibleTrackingColumns.map(item => ({
columnProp: item.columnProp,
columnLabel: item.columnLabel
}))
const exportProcessColumns = this.visibleProcessColumns.map(item => ({
code: item.code,
label: item.label,
planField: item.planField,
actualField: item.actualField,
statusField: item.statusField,
processCategoryCode: item.processCategoryCode,
processCategoryName: item.processCategoryName
}))
if (exportTrackingColumns.length === 0 && exportProcessColumns.length === 0) {
this.$message.warning('当前没有可导出的列')
return
}
const normalizedProofingStatusList = this.normalizeSearchProofingStatusList(this.searchData.proofingStatusList)
this.searchData.proofingStatusList = normalizedProofingStatusList
const inData = Object.assign({}, this.searchData, {
buNo: this.normalizeBuNo(this.searchData.buNo, this.searchData.site),
exportTrackingColumns: exportTrackingColumns,
exportProcessColumns: exportProcessColumns,
exportCommentsColumn: true
})
inData.proofingStatusList = normalizedProofingStatusList
this.applyProofTrackingSearchFilters(inData)
this.exportLoading = true
try {
const response = await exportProofTracking(inData)
const blob = response && response.data ? response.data : null
if (!blob) {
this.$message.error('导出失败')
return
}
if (blob.type && blob.type.indexOf('application/json') > -1) {
const errorText = await blob.text()
let errorMsg = '导出失败'
try {
const errorObj = JSON.parse(errorText)
if (errorObj && errorObj.msg) {
errorMsg = errorObj.msg
}
} catch (e) {
}
this.$message.error(errorMsg)
return
}
const fileName = this.resolveProofTrackingExportFileName(response)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
link.remove()
window.URL.revokeObjectURL(url)
} catch (e) {
this.$message.error('导出失败')
} finally {
this.exportLoading = false
}
},
resolveProofTrackingExportFileName (response) {
const defaultFileName = `项目打样跟踪_${this.getExportTimestamp()}.xlsx`
const disposition = response && response.headers
? (response.headers['content-disposition'] || response.headers['Content-Disposition'] || '')
: ''
if (!disposition) {
return defaultFileName
}
const utf8Match = disposition.match(/filename\*=utf-8''([^;]+)/i)
if (utf8Match && utf8Match[1]) {
try {
return decodeURIComponent(utf8Match[1])
} catch (e) {
return utf8Match[1]
}
}
const normalMatch = disposition.match(/filename="?([^";]+)"?/i)
if (normalMatch && normalMatch[1]) {
return normalMatch[1]
}
return defaultFileName
},
getExportTimestamp () {
const now = new Date()
const pad = num => String(num).padStart(2, '0')
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
},
resetSearch () {
this.searchData.projectNo = ''
this.searchData.projectDesc = ''
this.searchData.testPartNo = ''
this.searchData.partDesc = ''
this.searchData.dryWetType = ''
this.searchData.customerNo = ''
this.searchData.proofingNo = ''
this.searchData.cProjectRegion = ''
this.searchData.engineer = ''
this.searchData.projectManager = ''
this.searchData.pic = ''
this.searchData.projectPartSyncFlag = ''
this.searchData.proofSyncFlag = ''
this.searchData.proofingStatusList = []
this.searchData.sampleQty = ''
this.searchData.sampleMakingYear = ''
this.searchData.sampleMakingMonth = ''
this.searchData.pmInqueryTimeStart = ''
this.searchData.pmInqueryTimeEnd = ''
this.ensureSearchBuNoSelected()
this.queryProcessColumns().then(() => {
this.getDataList('Y')
})
},
normalizeSearchRoleOptions (rows) {
const optionMap = {}
const options = []
;(Array.isArray(rows) ? rows : []).forEach(item => {
if (!item || this.isBlankValue(item.username)) {
return
}
const username = String(item.username).trim()
const userDisplay = this.isBlankValue(item.userDisplay) ? '' : String(item.userDisplay).trim()
const value = userDisplay ? (username + '-' + userDisplay) : username
if (optionMap[value]) {
return
}
optionMap[value] = true
options.push({
value: value,
label: userDisplay || username,
username: username,
userDisplay: userDisplay
})
})
return options.sort((left, right) => String(left.label).localeCompare(String(right.label)))
},
loadSearchRoleOptions () {
// 与一键创建人员弹窗同源:Engineer / PM/Sales / PIC(PjM) 分别对应角色接口 6 / 0 / 1。
const roleQuery = { active: 'Y', limit: 1000 }
return Promise.all([
this.fetchRoleListByApiKey('6', roleQuery),
this.fetchRoleListByApiKey('0', roleQuery),
this.fetchRoleListByApiKey('1', roleQuery)
]).then(([engineerRows, projectManagerRows, picRows]) => {
this.searchEngineerOptions = this.normalizeSearchRoleOptions(engineerRows)
this.searchProjectManagerOptions = this.normalizeSearchRoleOptions(projectManagerRows)
this.searchPicOptions = this.normalizeSearchRoleOptions(picRows)
}).catch(() => {
this.searchEngineerOptions = []
this.searchProjectManagerOptions = []
this.searchPicOptions = []
})
},
selectionChange (rows) {
this.selectionRows = rows || []
},
isCommentsEditing (row) {
return !!row && row.trackingId === this.commentsEditingTrackingId
},
startCommentsEdit (row) {
if (!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保存异常')
})
},
handleTrackingTableSortChange (sortData) {
this.trackingTableSortProp = sortData && sortData.prop ? String(sortData.prop) : ''
this.trackingTableSortOrder = sortData && sortData.order ? String(sortData.order) : ''
this.applyTrackingTableSort()
this.refreshTrackingTableLayout()
},
resolveTrackingTableSortValue (row, prop) {
if (!row || !prop) {
return ''
}
const rawValue = row[prop]
const formattedValue = this.formatDate(rawValue)
if (formattedValue) {
return formattedValue
}
return rawValue == null ? '' : String(rawValue).trim()
},
compareDataListOrderIndex (a, b) {
const aOrderIndex = Number(a && a._pageOrderIndex != null ? a._pageOrderIndex : Number.MAX_SAFE_INTEGER)
const bOrderIndex = Number(b && b._pageOrderIndex != null ? b._pageOrderIndex : Number.MAX_SAFE_INTEGER)
return aOrderIndex - bOrderIndex
},
resetDataListPageOrderIndexes () {
if (!Array.isArray(this.dataList) || this.dataList.length === 0) {
return
}
this.dataList.forEach((row, index) => {
if (!row) {
return
}
this.$set(row, '_pageOrderIndex', index)
})
},
restoreDataListPageOrder () {
if (!Array.isArray(this.dataList) || this.dataList.length <= 1) {
return
}
this.dataList = this.dataList.slice().sort((a, b) => this.compareDataListOrderIndex(a, b))
},
applyTrackingTableSort () {
if (!Array.isArray(this.dataList) || this.dataList.length <= 1) {
return
}
const sortProp = this.trackingTableSortProp
const sortOrder = this.trackingTableSortOrder
if (!sortProp || !sortOrder) {
this.restoreDataListPageOrder()
return
}
const isAscending = sortOrder === 'ascending'
this.dataList = this.dataList.slice().sort((a, b) => {
const aValue = this.resolveTrackingTableSortValue(a, sortProp)
const bValue = this.resolveTrackingTableSortValue(b, sortProp)
const aEmpty = !aValue
const bEmpty = !bValue
if (aEmpty && bEmpty) {
return this.compareDataListOrderIndex(a, b)
}
if (aEmpty) {
return 1
}
if (bEmpty) {
return -1
}
if (aValue === bValue) {
return this.compareDataListOrderIndex(a, b)
}
if (isAscending) {
return aValue > bValue ? 1 : -1
}
return aValue < bValue ? 1 : -1
})
},
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)
},
isTrackingColumnSynced (row, columnProp) {
if (columnProp === 'proofingNo') {
return this.isProofSynced(row)
}
return this.isProjectPartSynced(row)
},
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() === ''
},
getApplyQuantityDisplayText (applyQuantity) {
if (this.isBlankValue(applyQuantity)) {
return '未维护'
}
return String(applyQuantity).trim()
},
normalizeProjectNo (projectNo) {
if (this.isBlankValue(projectNo)) {
return ''
}
return String(projectNo).trim()
},
queryApplyOptionsByProjectNo (projectNo) {
// 固定取项目编码前10位因为工程系统中项目编码前10位是唯一的,我们这边可能会有后缀或扩展位。
const normalizedProjectNo = projectNo.substring(0, 10);
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 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),
createTime: this.formatDate(item.createTime),
applyQuantity: this.isBlankValue(item.quantityReq) ? '' : String(item.quantityReq).trim()
})
})
return optionRows
})
},
handleOneKeyProjectNoBlur () {
const projectNo = this.normalizeProjectNo(this.oneKeyForm.projectNo)
const previousProjectNo = this.normalizeProjectNo(this.oneKeyProjectNoSnapshot)
this.oneKeyForm.projectNo = projectNo
if (!projectNo) {
this.oneKeyProofingApplyQueryToken += 1
this.oneKeyProofingApplyLoading = false
this.oneKeyProofingApplyOptions = []
this.oneKeyForm.proofingNo = ''
this.oneKeyForm.requiredDeliveryDate = ''
this.oneKeyForm.planStartDate = ''
this.oneKeyProjectNoSnapshot = ''
this.oneKeyForm.testPartNo = ''
this.oneKeyForm.partDesc = ''
this.oneKeyForm.partType = ''
this.oneKeyForm.dry_wet_type = ''
this.oneKeyPartNoSnapshot = ''
return
}
if (previousProjectNo.toUpperCase() !== projectNo.toUpperCase()) {
// 项目编码变化后需清理旧料号,避免跨项目保留旧料号与角色信息。
this.oneKeyForm.testPartNo = ''
this.oneKeyForm.partDesc = ''
this.oneKeyForm.partType = ''
this.oneKeyForm.dry_wet_type = ''
this.oneKeyPartNoSnapshot = ''
}
this.oneKeyProjectNoSnapshot = projectNo
this.loadOneKeyProofingApplyOptions(projectNo, { keepCurrentIfMissing: false })
},
handleOneKeyProofingNoChange (proofingNo) {
const selectedNo = this.isBlankValue(proofingNo) ? '' : String(proofingNo).trim()
if (this.oneKeyDialogMode !== 'create') {
// 修改模式下预计完成日期、打样开始日期以数据库已存值为准,不跟随打样单号自动回填/清空。
if (!selectedNo) {
this.oneKeyForm.proofingNo = ''
return
}
const matchedInEdit = this.oneKeyProofingApplyOptions.find(item => item && item.applyNo === selectedNo)
if (matchedInEdit) {
this.oneKeyForm.proofingNo = matchedInEdit.applyNo
}
return
}
if (!selectedNo) {
this.oneKeyForm.requiredDeliveryDate = ''
this.oneKeyForm.planStartDate = ''
return
}
const matched = this.oneKeyProofingApplyOptions.find(item => item && item.applyNo === selectedNo)
if (!matched) {
return
}
this.oneKeyForm.proofingNo = matched.applyNo
this.oneKeyForm.requiredDeliveryDate = matched.expectedFinishDate || ''
// 打样开始日期默认取工程实验单创建日期;清空打样单时同步清空,避免残留旧单日期。
this.oneKeyForm.planStartDate = matched.createTime || ''
this.syncOneKeyBaselineByRequiredDeliveryDate(this.oneKeyForm.requiredDeliveryDate, false)
},
handleOneKeyRequiredDeliveryDateChange (dateVal) {
this.syncOneKeyBaselineByRequiredDeliveryDate(dateVal, false)
},
syncOneKeyBaselineByRequiredDeliveryDate (requiredDeliveryDate, forceSync) {
const normalizedRequiredDate = this.formatDate(requiredDeliveryDate)
if (!normalizedRequiredDate) {
return
}
const normalizedBaseline = this.formatDate(this.oneKeyForm.baseline)
if (forceSync || !normalizedBaseline) {
// baseline 首次默认跟随 Delivery&Package 日期,后续可按业务独立调整。
this.oneKeyForm.baseline = normalizedRequiredDate
}
},
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),
createTime: this.formatDate(this.oneKeyForm.planStartDate),
applyQuantity: ''
})
}
if (!hasCurrent && !keepCurrentIfMissing) {
this.oneKeyForm.proofingNo = ''
this.oneKeyForm.requiredDeliveryDate = ''
this.oneKeyForm.planStartDate = ''
}
}
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 = ''
this.proofDialogData.planStartDate = ''
return
}
const matched = this.proofDialogApplyOptions.find(item => item && item.applyNo === selectedNo)
if (!matched) {
return
}
this.proofDialogData.proofingNo = matched.applyNo
this.proofDialogData.requiredDeliveryDate = matched.expectedFinishDate || ''
// 打样开始日期默认取工程实验单创建日期;清空打样单时同步清空,避免残留旧单日期。
this.proofDialogData.planStartDate = matched.createTime || ''
},
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 = ''
this.proofDialogData.planStartDate = ''
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
},
normalizeRoleValue (value) {
if (this.isBlankValue(value)) {
return ''
}
return String(value).trim()
},
getSiteByJoinedBuNo (buNo) {
const rawBuNo = buNo == null ? '' : String(buNo).trim()
if (!rawBuNo) {
return this.$store.state.user.site
}
const splitIdx = rawBuNo.indexOf('_')
if (splitIdx > 0) {
return rawBuNo.substring(0, splitIdx)
}
return this.$store.state.user.site
},
getOneKeyBuQueryContext () {
const currentBuNo = this.oneKeyForm && this.oneKeyForm.buNo
? String(this.oneKeyForm.buNo).trim()
: ''
const normalizedBuNo = this.normalizeBuNo(currentBuNo, this.$store.state.user.site)
if (!normalizedBuNo) {
return null
}
const matchedBu = this.userBuList.find(item => {
if (!item || !item.buNo) {
return false
}
const itemBuNo = this.normalizeBuNo(item.buNo, this.$store.state.user.site)
return itemBuNo.toUpperCase() === normalizedBuNo.toUpperCase()
}) || null
const joinedBuNo = matchedBu && matchedBu.buNo ? matchedBu.buNo : currentBuNo
return {
normalizedBuNo: normalizedBuNo,
buDesc: matchedBu && matchedBu.buDesc ? String(matchedBu.buDesc).trim() : '',
site: this.getSiteByJoinedBuNo(joinedBuNo)
}
},
isOneKeyDryWetTypeVisible () {
return this.isRfidBuNo(this.oneKeyForm && this.oneKeyForm.buNo ? this.oneKeyForm.buNo : '')
},
handleOneKeyBuChange () {
// 干湿分类仅用于 RFID,切换到其他 BU 时主动清空,避免隐藏字段误提交。
if (!this.isOneKeyDryWetTypeVisible()) {
this.oneKeyForm.dry_wet_type = ''
}
},
getRowDateSortTime (row, fieldList) {
if (!row || !Array.isArray(fieldList)) {
return 0
}
for (let i = 0; i < fieldList.length; i += 1) {
const fieldKey = fieldList[i]
const rawVal = row[fieldKey]
if (this.isBlankValue(rawVal)) {
continue
}
const date = new Date(rawVal)
if (isNaN(date.getTime())) {
continue
}
return date.getTime()
}
return 0
},
sortRowsByDateDesc (rows, fieldList) {
const sourceRows = Array.isArray(rows) ? rows : []
return sourceRows.slice().sort((a, b) => {
const diff = this.getRowDateSortTime(b, fieldList) - this.getRowDateSortTime(a, fieldList)
if (diff !== 0) {
return diff
}
const leftKey = this.isBlankValue(a && a.projectNo) ? (a && a.testPartNo ? String(a.testPartNo) : '') : String(a.projectNo)
const rightKey = this.isBlankValue(b && b.projectNo) ? (b && b.testPartNo ? String(b.testPartNo) : '') : String(b.projectNo)
return leftKey.localeCompare(rightKey)
})
},
getOneKeyDescSearchKeyword (queryString, selectedSnapshotCode) {
const keyword = this.isBlankValue(queryString) ? '' : String(queryString).trim()
if (!keyword) {
return ''
}
const snapshotCode = this.isBlankValue(selectedSnapshotCode) ? '' : String(selectedSnapshotCode).trim()
// 只能和“上次选中的编码”比较。输入框 v-model 就是当前关键字,拿它比较会把描述条件清掉。
if (snapshotCode && keyword.toUpperCase() === snapshotCode.toUpperCase()) {
return ''
}
return keyword
},
matchOneKeyDescKeyword (sourceText, keyword) {
if (this.isBlankValue(keyword)) {
return true
}
if (this.isBlankValue(sourceText)) {
return false
}
return String(sourceText).toUpperCase().indexOf(String(keyword).trim().toUpperCase()) > -1
},
queryOneKeyProjectOptions (projectDescKeyword) {
const buContext = this.getOneKeyBuQueryContext()
if (!buContext) {
return Promise.resolve([])
}
const queryKeyword = this.isBlankValue(projectDescKeyword) ? '' : String(projectDescKeyword).trim()
const inData = {
site: buContext.site,
userName: this.$store.state.user.name,
buDesc: buContext.buDesc,
projectNo: '',
projectDesc: queryKeyword,
customerNo: '',
customerDesc: '',
projectCategory: '',
status: '',
projectManager: '',
projectOwner: '',
engineer: '',
cProjectRegion: '',
startDate: '',
endDate: '',
page: 1,
limit: 1000
}
return eamProjectInfoSearch(inData).then(({ data }) => {
if (!(data && data.code === 0)) {
return []
}
const sourceRows = (data.page && data.page.list) || []
const targetBuNo = buContext.normalizedBuNo.toUpperCase()
const targetBuDesc = this.isBlankValue(buContext.buDesc) ? '' : String(buContext.buDesc).trim().toUpperCase()
const filteredRows = sourceRows.filter(item => {
if (!item || this.isBlankValue(item.projectNo)) {
return false
}
const rowBuNo = this.normalizeBuNo(item.buNo || item.bu || '', buContext.site).toUpperCase()
const rowBuDesc = this.isBlankValue(item.buDesc) ? '' : String(item.buDesc).trim().toUpperCase()
const matchedBu = (targetBuNo && rowBuNo && rowBuNo === targetBuNo) || (targetBuDesc && rowBuDesc && rowBuDesc === targetBuDesc)
const hasRowBuInfo = !this.isBlankValue(item.buNo || item.bu || item.buDesc)
if (hasRowBuInfo && !matchedBu) {
return false
}
return this.matchOneKeyDescKeyword(item.projectDesc, queryKeyword)
|| this.matchOneKeyDescKeyword(item.projectName, queryKeyword)
})
return this.sortRowsByDateDesc(filteredRows, ['createDate', 'projectCreationDate', 'updateDate'])
}).catch(() => {
return []
})
},
queryOneKeyPartOptions (projectNo, partDescKeyword) {
const normalizedProjectNo = this.normalizeProjectNo(projectNo)
if (!normalizedProjectNo) {
return Promise.resolve([])
}
const buContext = this.getOneKeyBuQueryContext()
const queryKeyword = this.isBlankValue(partDescKeyword) ? '' : String(partDescKeyword).trim()
const inData = {
site: buContext ? buContext.site : this.$store.state.user.site,
userName: this.$store.state.user.name,
buDesc: buContext ? buContext.buDesc : '',
projectNo: normalizedProjectNo,
projectDesc: '',
testPartNo: '',
partDesc: queryKeyword,
customerNo: '',
customerDesc: '',
projectCategory: '',
status: '',
startDate: '',
endDate: '',
projectManager: '',
projectOwner: '',
engineer: '',
cProjectRegion: '',
finalPartNo: '',
massProductionStartDate: '',
massProductionEndDate: '',
feedingFlag: '0',
page: 1,
limit: 1000
}
return eamProjectPartSearch(inData).then(({ data }) => {
if (!(data && data.code === 0)) {
return []
}
const sourceRows = (data.page && data.page.list) || []
const targetProjectNo = normalizedProjectNo.toUpperCase()
const filteredRows = sourceRows.filter(item => {
const rowPartNo = item && item.testPartNo ? String(item.testPartNo).trim() : ''
const rowProjectNo = this.normalizeProjectNo(item && item.projectNo ? item.projectNo : '').toUpperCase()
if (!rowPartNo || rowProjectNo !== targetProjectNo) {
return false
}
return this.matchOneKeyDescKeyword(item.partDesc, queryKeyword)
})
return this.sortRowsByDateDesc(filteredRows, ['createDate', 'updateDate', 'buildDate'])
}).catch(() => {
return []
})
},
queryOneKeyProjectSuggestions (queryString, callback) {
const queryToken = this.oneKeyProjectQueryToken + 1
this.oneKeyProjectQueryToken = queryToken
const descKeyword = this.getOneKeyDescSearchKeyword(queryString, this.oneKeyProjectNoSnapshot)
this.queryOneKeyProjectOptions(descKeyword).then(rows => {
if (queryToken !== this.oneKeyProjectQueryToken) {
return
}
const suggestionRows = rows.map(item => {
const projectNo = this.normalizeProjectNo(item.projectNo)
if (!projectNo) {
return null
}
return {
value: projectNo,
projectDesc: item.projectDesc || item.projectName || '',
customerDesc: item.customerDesc || '',
createDateText: this.formatDate(item.createDate || item.projectCreationDate || item.updateDate),
source: item
}
}).filter(item => !!item)
callback(suggestionRows)
}).catch(() => {
if (queryToken === this.oneKeyProjectQueryToken) {
callback([])
}
})
},
queryOneKeyPartSuggestions (queryString, callback) {
const projectNo = this.normalizeProjectNo(this.oneKeyProjectNoSnapshot)
if (!projectNo) {
callback([])
return
}
const queryToken = this.oneKeyPartQueryToken + 1
this.oneKeyPartQueryToken = queryToken
const descKeyword = this.getOneKeyDescSearchKeyword(queryString, this.oneKeyPartNoSnapshot)
this.queryOneKeyPartOptions(projectNo, descKeyword).then(rows => {
if (queryToken !== this.oneKeyPartQueryToken) {
return
}
const suggestionRows = rows.map(item => {
const partNo = item && item.testPartNo ? String(item.testPartNo).trim() : ''
if (!partNo) {
return null
}
return {
value: partNo,
partDesc: item.partDesc || '',
createDateText: this.formatDate(item.createDate || item.updateDate || item.buildDate),
source: item
}
}).filter(item => !!item)
callback(suggestionRows)
}).catch(() => {
if (queryToken === this.oneKeyPartQueryToken) {
callback([])
}
})
},
setOneKeyRoleFieldValue (fieldKey, roleValue) {
const normalizedRoleValue = this.normalizeRoleValue(roleValue)
this.oneKeyForm[fieldKey] = normalizedRoleValue
const roleCfg = this.roleConfig[fieldKey]
if (roleCfg && roleCfg.nameField) {
this.oneKeyForm[roleCfg.nameField] = this.getRoleDisplayName(normalizedRoleValue)
}
},
setOneKeyRoleFieldsFromSource (source) {
const roleSource = source || {}
ONE_KEY_ROLE_FIELDS.forEach(fieldKey => {
this.setOneKeyRoleFieldValue(fieldKey, roleSource[fieldKey])
})
},
snapshotOneKeyProjectRoles (source) {
const roleSource = source || {}
const roleBaseline = {}
ONE_KEY_ROLE_FIELDS.forEach(fieldKey => {
roleBaseline[fieldKey] = this.normalizeRoleValue(roleSource[fieldKey])
})
this.oneKeyProjectRoleBaseline = roleBaseline
},
syncOneKeyPartRoleFields (partRow) {
const changedRoleFields = []
ONE_KEY_ROLE_FIELDS.forEach(fieldKey => {
const partRoleValue = this.normalizeRoleValue(partRow && partRow[fieldKey])
if (!partRoleValue) {
return
}
const baselineRoleValue = this.normalizeRoleValue(this.oneKeyProjectRoleBaseline[fieldKey])
if (baselineRoleValue === partRoleValue) {
return
}
const currentRoleValue = this.normalizeRoleValue(this.oneKeyForm[fieldKey])
if (currentRoleValue === partRoleValue) {
return
}
this.setOneKeyRoleFieldValue(fieldKey, partRoleValue)
changedRoleFields.push(fieldKey)
})
if (changedRoleFields.length > 0) {
this.$message.info('项目料号角色人员有变动,已同步角色账号')
}
},
applyOneKeyProjectSelection (projectRow) {
const selectedProject = projectRow || {}
const projectNo = this.normalizeProjectNo(selectedProject.projectNo)
if (!projectNo) {
return
}
const previousProjectNo = this.normalizeProjectNo(this.oneKeyProjectNoSnapshot)
const projectChanged = previousProjectNo.toUpperCase() !== projectNo.toUpperCase()
this.oneKeyForm.projectNo = projectNo
this.oneKeyProjectNoSnapshot = projectNo
this.oneKeyForm.projectName = selectedProject.projectName || selectedProject.projectDesc || ''
this.oneKeyForm.projectDesc = selectedProject.projectDesc || selectedProject.projectName || ''
this.oneKeyForm.customerNo = this.isBlankValue(selectedProject.customerNo) ? '' : String(selectedProject.customerNo).trim()
this.oneKeyForm.customerDesc = selectedProject.customerDesc || ''
this.oneKeyForm.projectCreationDate = this.formatDate(selectedProject.projectCreationDate || selectedProject.createDate)
this.oneKeyForm.needDate = this.formatDate(selectedProject.needDate)
this.oneKeyForm.projectCategory = this.getProjectCategoryValue(selectedProject)
this.oneKeyForm.cProjectRegion = this.isBlankValue(selectedProject.cProjectRegion) ? '' : String(selectedProject.cProjectRegion).trim()
const rawPriority = this.isBlankValue(selectedProject.priorityLevel) ? selectedProject.priority : selectedProject.priorityLevel
this.oneKeyForm.priorityLevel = this.isBlankValue(rawPriority) ? '' : String(rawPriority).trim()
this.setOneKeyRoleFieldsFromSource(selectedProject)
this.snapshotOneKeyProjectRoles(selectedProject)
if (projectChanged) {
// 项目切换时清理料号相关字段,防止旧料号角色残留到新项目。
this.oneKeyForm.testPartNo = ''
this.oneKeyForm.partDesc = ''
this.oneKeyForm.partType = ''
this.oneKeyForm.dry_wet_type = ''
this.oneKeyPartNoSnapshot = ''
}
this.loadOneKeyProofingApplyOptions(projectNo, { keepCurrentIfMissing: false })
},
applyOneKeyPartSelection (partRow) {
const selectedPart = partRow || {}
const partNo = this.isBlankValue(selectedPart.testPartNo) ? '' : String(selectedPart.testPartNo).trim()
if (!partNo) {
return
}
this.oneKeyForm.testPartNo = partNo
this.oneKeyPartNoSnapshot = partNo
if (!this.isBlankValue(selectedPart.partDesc)) {
this.oneKeyForm.partDesc = String(selectedPart.partDesc).trim()
}
if (!this.isBlankValue(selectedPart.partType)) {
this.oneKeyForm.partType = String(selectedPart.partType).trim()
}
// 接口可能返回 camelCase 或下划线字段名,这里统一兼容后回填到表单。
const selectedDryWetType = this.isBlankValue(selectedPart.dry_wet_type)
? selectedPart.dryWetType
: selectedPart.dry_wet_type
if (this.isOneKeyDryWetTypeVisible() && !this.isBlankValue(selectedDryWetType)) {
this.oneKeyForm.dry_wet_type = String(selectedDryWetType).trim()
} else if (!this.isOneKeyDryWetTypeVisible()) {
this.oneKeyForm.dry_wet_type = ''
}
// 业务要求:料号角色与项目角色存在差异时,界面需优先回填料号角色账号。
this.syncOneKeyPartRoleFields(selectedPart)
},
handleOneKeyProjectSelect (item) {
const selectedRow = item && item.source ? item.source : null
if (!selectedRow) {
return
}
this.applyOneKeyProjectSelection(selectedRow)
},
handleOneKeyPartSelect (item) {
const selectedRow = item && item.source ? item.source : null
if (!selectedRow) {
return
}
this.applyOneKeyPartSelection(selectedRow)
},
handleOneKeyPartNoBlur () {
this.oneKeyForm.testPartNo = this.isBlankValue(this.oneKeyForm.testPartNo)
? ''
: String(this.oneKeyForm.testPartNo).trim()
},
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.oneKeyDialogMode === 'create' && this.isBlankValue(inData.baseline) && !this.isBlankValue(inData.requiredDeliveryDate)) {
// 一键创建若已带出 Delivery&Package 日期,则 baseline 默认同步该日期(允许用户提交前改写)。
inData.baseline = this.formatDate(inData.requiredDeliveryDate)
}
// 干湿分类仅在 RFID 使用,非 RFID 提交时统一清空,避免旧值串到错误 BU。
if (!this.isRfidBuNo(inData.buNo)) {
inData.dry_wet_type = ''
}
// 弹窗仅展示一个立项日期输入,提交时同步到项目与物料两个日期字段,避免物料立项日期被误判为空
if (!this.isBlankValue(inData.projectCreationDate)) {
inData.buildDate = inData.projectCreationDate
} else if (!this.isBlankValue(inData.buildDate)) {
inData.projectCreationDate = inData.buildDate
}
inData.pmInqueryTime = this.isBlankValue(inData.pmInqueryTime) ? null : this.formatDate(inData.pmInqueryTime)
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 || '',
dry_wet_type: part.dry_wet_type || part.dryWetType || current.dry_wet_type || current.dryWetType || '',
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 || '',
baseline: current.baseline || '',
pmInqueryTime: this.formatDate(current.pmInqueryTime),
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)
if (!this.isRfidBuNo(form.buNo)) {
form.dry_wet_type = ''
}
return form
},
openOneKeyDialog (mode, row) {
const dialogMode = mode === 'edit' ? 'edit' : (mode === 'detail' ? 'detail' : 'create')
this.oneKeyDialogMode = dialogMode
const actionText = dialogMode === 'detail' ? '查看详情' : '修改'
if (dialogMode === 'create') {
this.oneKeyProofingApplyQueryToken += 1
this.oneKeyProofingApplyLoading = false
this.oneKeyProofingApplyOptions = []
this.oneKeyProjectQueryToken += 1
this.oneKeyPartQueryToken += 1
this.oneKeyProjectNoSnapshot = ''
this.oneKeyPartNoSnapshot = ''
this.oneKeyProjectRoleBaseline = {}
this.oneKeyForm = this.getEmptyOneKeyForm()
const selectedSearchBuNo = this.getSelectedSearchBuNo()
if (selectedSearchBuNo) {
// 一键创建默认沿用查询区 BU,避免用户重复选择。
this.oneKeyForm.buNo = this.getBuOptionValue(this.searchData.site, selectedSearchBuNo)
} else if (this.userBuList.length > 0) {
this.oneKeyForm.buNo = this.userBuList[0].buNo || ''
}
this.handleOneKeyBuChange()
this.oneKeyDialogVisible = true
return
}
const current = this.getActionRow(row)
if (!current) {
this.$message.warning(`请先选择一条记录后再${actionText}项目/物料/打样`)
this.oneKeyDialogMode = 'create'
return
}
const projectPartId = this.getTrackingProjectPartId(current)
if (!current.projectId || !projectPartId || !current.trackingId) {
this.$message.warning(`当前记录缺少项目/物料/打样关键信息,无法${actionText}`)
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(`未找到对应的项目信息,无法${actionText}`)
}
if (partRows.length === 0) {
throw new Error(`未找到对应的项目物料信息,无法${actionText}`)
}
this.oneKeyForm = this.buildOneKeyEditForm(current, projectRows[0], partRows[0])
this.snapshotOneKeyProjectRoles(projectRows[0])
this.oneKeyProjectNoSnapshot = this.normalizeProjectNo(this.oneKeyForm.projectNo)
this.oneKeyPartNoSnapshot = this.isBlankValue(this.oneKeyForm.testPartNo) ? '' : String(this.oneKeyForm.testPartNo).trim()
this.oneKeyProofingApplyQueryToken += 1
this.oneKeyProofingApplyLoading = false
this.oneKeyProofingApplyOptions = []
this.oneKeyProjectQueryToken += 1
this.oneKeyPartQueryToken += 1
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.dryWetType = ''
this.searchData.customerNo = ''
this.searchData.proofingNo = ''
this.searchData.projectPartSyncFlag = ''
this.searchData.proofSyncFlag = ''
this.searchData.proofingStatusList = []
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: '',
pmInqueryTime: '',
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)
},
collectSyncToNpiMissingLabels (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('料号描述')
}
return missingLabels
},
getSelectedRowsForBatchSync (actionLabel) {
const rows = this.selectionRows && this.selectionRows.length > 0 ? this.selectionRows : []
if (rows.length === 0) {
this.$message.warning(`请先勾选需要${actionLabel}的记录`)
}
return rows
},
syncBatchProjectPartToNpi () {
const selectedRows = this.getSelectedRowsForBatchSync('同步项目物料到NPI')
if (selectedRows.length === 0) {
return
}
const idMap = {}
const projectPartIds = []
const invalidRows = []
selectedRows.forEach(row => {
const projectPartId = this.getTrackingProjectPartId(row)
const missingLabels = this.collectSyncToNpiMissingLabels(row)
if (!projectPartId || missingLabels.length > 0) {
invalidRows.push({
projectNo: row && row.projectNo ? row.projectNo : '-',
testPartNo: row && row.testPartNo ? row.testPartNo : '-',
missingLabels: missingLabels.length > 0 ? missingLabels : ['项目物料ID']
})
return
}
const idKey = String(projectPartId)
if (idMap[idKey]) {
return
}
idMap[idKey] = true
projectPartIds.push(projectPartId)
})
if (projectPartIds.length === 0) {
this.$message.warning('勾选记录未包含可同步的项目物料')
return
}
const invalidHint = invalidRows.length > 0
? `<br/>其中${invalidRows.length}条记录不满足同步条件,已自动跳过`
: ''
const confirmMsg = `确认批量同步项目物料到NPI吗?<br/>已勾选${selectedRows.length}条,可同步${projectPartIds.length}${invalidHint}`
this.$confirm(confirmMsg, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true
}).then(() => {
const operator = this.$store.state.user.name
this.syncBatchNpiLoading = true
batchSyncProjectPartTracking({
projectPartIds: projectPartIds,
updateBy: operator,
createBy: operator,
userName: operator
}).then(({ data }) => {
if (data && data.code === 0) {
const syncedCount = data.syncedCount || projectPartIds.length
this.$message.success(`批量同步成功,共${syncedCount}条项目物料`)
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.syncBatchNpiLoading = false
})
}).catch(() => {})
},
syncBatchProofToNpi () {
const selectedRows = this.getSelectedRowsForBatchSync('同步打样到NPI')
if (selectedRows.length === 0) {
return
}
const idMap = {}
const trackingIds = []
const invalidRows = []
selectedRows.forEach(row => {
const trackingId = row && row.trackingId
if (!trackingId) {
invalidRows.push({
projectNo: row && row.projectNo ? row.projectNo : '-',
testPartNo: row && row.testPartNo ? row.testPartNo : '-',
proofingNo: row && row.proofingNo ? row.proofingNo : '-'
})
return
}
const idKey = String(trackingId)
if (idMap[idKey]) {
return
}
idMap[idKey] = true
trackingIds.push(trackingId)
})
if (trackingIds.length === 0) {
this.$message.warning('勾选记录未包含可同步的打样单')
return
}
const invalidHint = invalidRows.length > 0
? `<br/>其中${invalidRows.length}条记录缺少打样ID,已自动跳过`
: ''
const confirmMsg = `确认批量同步打样到NPI吗?<br/>已勾选${selectedRows.length}条,可同步${trackingIds.length}${invalidHint}`
this.$confirm(confirmMsg, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true
}).then(() => {
const operator = this.$store.state.user.name
this.syncBatchProofLoading = true
batchSyncProofTracking({
trackingIds: trackingIds,
updateBy: operator,
createBy: operator,
userName: operator
}).then(({ data }) => {
if (data && data.code === 0) {
const syncedCount = data.syncedCount || trackingIds.length
this.$message.success(`批量同步成功,共${syncedCount}条打样记录`)
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.syncBatchProofLoading = 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 || null,
pmInqueryTime: this.isBlankValue(this.proofDialogData.pmInqueryTime) ? null : this.proofDialogData.pmInqueryTime,
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('新增打样异常')
})
},
handleProcessDateChange (row, processCol, dateVal) {
if (!row || !processCol) {
return
}
if (this.isTrackingRowProcessReadonly(row)) {
return
}
const actualDate = this.formatDate(dateVal)
// 业务要求:表格选择日期仅更新日期,不自动改变工序状态(状态由“√”动作控制)。
const currentStatus = row[processCol.statusField] ? String(row[processCol.statusField]).trim() : ''
const nextStatus = currentStatus || '未完成'
this.updateProcessFromTable(row, processCol, {
status: nextStatus,
actualDate: actualDate,
remark: actualDate ? '表格直接修改实际完成日期(状态不变)' : '表格清空实际完成日期(状态不变)'
}, true)
},
markProcessComplete (row, processCol) {
if (!row || !processCol || this.isProcessStatusComplete(row, processCol)) {
return
}
if (this.isTrackingRowProcessReadonly(row)) {
return
}
// 若用户已手工选择日期,打勾仅改状态,不覆盖已选日期;无日期时才补当天。
const currentActualDate = this.formatDate(row[processCol.actualField])
const nextActualDate = currentActualDate || this.formatDate(new Date())
this.updateProcessFromTable(row, processCol, {
status: '已完成',
actualDate: nextActualDate,
remark: '表格一键完成'
}, false, '工序已标记为完成')
},
handleProcessDateDoubleClick (row, processCol) {
if (!row || !processCol || !this.isProcessStatusComplete(row, processCol)) {
return
}
if (this.isTrackingRowProcessReadonly(row) && !this.canRollbackProofingComplete(row, processCol)) {
return
}
// 按页面交互约定:已完成工序允许双击日期输入框进行回退。
this.rollbackProcessComplete(row, processCol)
},
rollbackProcessComplete (row, processCol) {
if (!row || !processCol || !this.isProcessStatusComplete(row, processCol)) {
return
}
const rollbackProofingComplete = this.canRollbackProofingComplete(row, processCol)
if (this.isTrackingRowProcessReadonly(row) && !rollbackProofingComplete) {
return
}
const currentActualDate = this.formatDate(row[processCol.actualField])
const proofingNoText = this.getRollbackProofingNoDisplay(row)
// Delivery&Package 在“打样完成”状态下的撤回需要同时回退整单状态,提示语单独说明影响范围。
const rollbackTip = rollbackProofingComplete
? `确定撤回打样单(${proofingNoText})的Delivery&Package已完成状态吗?撤回后打样状态恢复为“进行中”。`
: `确定回退该工序状态为“未完成”吗?打样单号:${proofingNoText}`
this.$confirm(rollbackTip, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const rollbackRemark = rollbackProofingComplete
? '撤回Delivery&Package已完成并回退打样状态'
: '表格一键回退为未完成'
const rollbackSuccessMsg = rollbackProofingComplete
? 'Delivery&Package已撤回,打样状态恢复为进行中'
: '工序状态已回退为未完成'
// 仅回退状态,保留当前实际完成日期,避免误回退时丢失用户已维护的日期信息。
this.updateProcessFromTable(row, processCol, {
status: '未完成',
actualDate: currentActualDate,
remark: rollbackRemark
}, false, rollbackSuccessMsg)
}).catch(() => {})
},
isDeliveryPackageProcess (processCol) {
if (!processCol || processCol.code == null) {
return false
}
return String(processCol.code).trim().toLowerCase() === 'deliverypackage'
},
canRollbackProofingComplete (row, processCol) {
if (!row || !processCol) {
return false
}
if (!this.isProofingFinishedForTrackingStatus(row)) {
return false
}
if (!this.isDeliveryPackageProcess(processCol)) {
return false
}
return this.isProcessStatusComplete(row, processCol)
},
getRollbackProofingNoDisplay (row) {
if (!row) {
return '-'
}
const proofingNo = row.proofingNo == null ? '' : String(row.proofingNo).trim()
if (proofingNo) {
return proofingNo
}
if (row.trackingId != null && row.trackingId !== '') {
return `TrackingID:${row.trackingId}`
}
return '-'
},
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 || '工序更新成功')
}
// 按业务要求:工序日期或状态变更后回查列表,保证动态列/派生字段展示最新值。
this.getDataList()
} else {
this.$message.error(data.msg || '工序更新失败')
this.getDataList()
}
}).catch(() => {
this.$message.error('工序更新异常')
this.getDataList()
})
},
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).then(() => {
showTooltip()
})
},
loadProcessHistory (row, processCol) {
if (!row || !row.trackingId) {
return Promise.resolve([])
}
const cacheKey = this.getProcessCacheKey(row, processCol)
if (Object.prototype.hasOwnProperty.call(this.processHistoryMap, cacheKey)) {
return Promise.resolve(this.processHistoryMap[cacheKey] || [])
}
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)
}
return rows
}).catch(() => {
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 <= 1) {
return ''
}
// tooltip 只展示历史日期,不展示最近一次日期(当前值)以避免重复。
const latestDate = dateList.reduce((max, currentDate) => {
if (!max || currentDate > max) {
return currentDate
}
return max
}, '')
const historyDateList = dateList.filter(item => item !== latestDate)
if (historyDateList.length === 0) {
return ''
}
return historyDateList.join('\n')
},
isProcessStatusComplete (row, processCol) {
if (!row || !processCol) {
return false
}
const statusVal = row[processCol.statusField]
const status = statusVal ? String(statusVal).trim() : ''
if (!status || status === '未完成' || status === '进行中') {
return false
}
if (status === '已完成' || status === '打样完成') {
return true
}
return status.indexOf('完成') > -1 && status.indexOf('未') === -1
},
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
},
getTrackingStatusProcessLabel (row, processCol) {
if (!row || !processCol) {
return ''
}
const processCategoryName = processCol.processCategoryName == null
? ''
: String(processCol.processCategoryName).trim()
if (processCategoryName) {
return processCategoryName
}
const processCategoryCode = this.getBuProcessCategoryCodeByProcessCode(row.buNo, processCol.code)
const buCategoryName = this.getBuProcessCategoryNameByCode(row.buNo, processCategoryCode)
if (buCategoryName) {
return buCategoryName
}
return processCol.label || processCol.code
},
getPendingProcessColumnForTrackingStatus (row) {
const processColumns = this.getTrackingProcessColumnsByRow(row)
let firstPendingWithDate = null
let firstPendingDate = ''
let firstPendingWithoutDate = null
for (let i = 0; i < processColumns.length; i += 1) {
const processCol = processColumns[i]
if (this.isProcessComplete(row, processCol)) {
continue
}
const actualDate = this.formatDate(row[processCol.actualField])
if (!actualDate) {
// 无日期的未完成工序仅作为兜底:优先展示“有日期的未完成工序”。
if (!firstPendingWithoutDate) {
firstPendingWithoutDate = processCol
}
continue
}
// 同日期按工序顺序兜底:仅在日期更小时替换,日期相同保留先命中的工序。
if (!firstPendingWithDate || actualDate < firstPendingDate) {
firstPendingWithDate = processCol
firstPendingDate = actualDate
}
}
return firstPendingWithDate || firstPendingWithoutDate
},
getFirstPendingProcessLabel (row) {
const processCol = this.getPendingProcessColumnForTrackingStatus(row)
if (!processCol) {
return '已全部完成'
}
return this.getTrackingStatusProcessLabel(row, processCol)
},
getTrackingPackageDate (row) {
if (!row) {
return ''
}
const requiredDeliveryDate = this.formatDate(row.requiredDeliveryDate)
if (requiredDeliveryDate) {
return requiredDeliveryDate
}
// requiredDeliveryDate 与 Delivery&Package 工序日期同一口径,主字段缺失时回退工序实际日期。
return this.formatDate(row.deliveryPackageActualDate)
},
getTrackingStatusType (row) {
if (!row) {
return ''
}
const packageDate = this.getTrackingPackageDate(row)
const baseline = this.formatDate(row.baseline)
// 缺少 package 或 baseline 时无法判定延期,保持空状态由文案函数显示 '-'。
if (!packageDate || !baseline) {
return ''
}
// Delay 按 Delivery&Package 相对原计划判定:package 晚于 baseline 即延期,
// 不再用“今天 vs 预计完成日期”,避免计划已改期后仍显示 On schedule。
if (packageDate > baseline) {
return 'delay'
}
return 'onSchedule'
},
isProofingFinishedForTrackingStatus (row) {
if (!row || this.isBlankValue(row.proofingStatus)) {
return false
}
const proofingStatus = String(row.proofingStatus).trim()
// 打样已结束时,Tracking Status 统一收敛为灰色 Complete,避免继续显示进度预警。
return proofingStatus === '打样完成'
},
isTrackingRowProcessReadonly (row) {
// Complete 行不再允许维护工序日期与状态,统一改为只读文本展示。
return this.isProofingFinishedForTrackingStatus(row)
},
getTrackingStatusText (row) {
if (this.isProofingFinishedForTrackingStatus(row)) {
return 'Complete'
}
const statusType = this.getTrackingStatusType(row)
if (!statusType) {
return '-'
}
const statusPrefix = statusType === 'onSchedule' ? 'On schedule' : 'Delay'
return `${statusPrefix}-${this.getFirstPendingProcessLabel(row)}`
},
getTrackingStatusClass (row) {
if (this.isProofingFinishedForTrackingStatus(row)) {
return 'tracking-status tracking-status--complete'
}
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 >
.el-table.el-table--medium.data-table th,
.el-table.data-table th {
height: 26px;
padding-top: 0;
padding-bottom: 0;
}
.el-table.data-table th .cell {
height: 26px;
line-height: 26px;
font-size: 12px;
}
.el-table.data-table td .cell {
height: 28px;
line-height: 28px;
font-size: 12px;
}
.search-form {
margin-bottom: 0;
}
.search-proofing-status-select .el-select__tags {
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
}
.search-proofing-status-select .el-select__tags > span {
display: inline-flex;
flex-wrap: nowrap;
}
.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(3, minmax(220px, 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;
gap: 6px;
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-order-item .el-checkbox {
flex: 1;
min-width: 0;
}
.bu-process-config-dialog .bu-process-category-select {
width: 100px;
flex-shrink: 0;
}
.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;
}
.default-process-config-dialog .default-process-config-actions {
margin-bottom: 8px;
}
.default-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;
}
.default-process-config-dialog .bu-process-config-tip i {
color: #409eff;
}
.default-process-config-dialog .default-process-config-empty {
margin: 12px 0;
padding: 12px;
border: 1px dashed #dcdfe6;
border-radius: 4px;
color: #909399;
text-align: center;
}
.default-process-config-dialog .default-process-drag-handle {
color: #909399;
cursor: move;
font-size: 15px;
margin-right: 6px;
}
.default-process-config-dialog .default-process-order-index {
color: #606266;
font-size: 12px;
}
.default-process-config-dialog .el-table__body-wrapper tbody tr {
cursor: move;
}
.default-process-config-dialog .el-table__body-wrapper tbody tr .el-input__inner {
cursor: text;
}
.default-process-config-dialog .el-table__body-wrapper tbody tr .el-checkbox__inner,
.default-process-config-dialog .el-table__body-wrapper tbody tr .el-select .el-input__inner {
cursor: pointer;
}
.process-category-config-dialog .default-process-config-actions {
margin-bottom: 8px;
}
.process-category-config-dialog .el-table__body-wrapper tbody tr .el-input__inner {
cursor: text;
}
.default-process-sortable-ghost > td {
background: #f0f9ff !important;
}
.default-process-sortable-chosen > td {
background: #ecf5ff !important;
}
.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;
}
.one-key-suggest-main {
color: #303133;
font-size: 12px;
line-height: 18px;
}
.one-key-suggest-sub {
color: #909399;
font-size: 12px;
line-height: 16px;
}
.proof-grid-form {
padding: 0 8px;
}
.proof-grid-form .el-form-item {
margin-bottom: 10px;
}
.apply-qty-reference {
margin-top: 6px;
padding: 4px 8px;
border-radius: 4px;
background: #ecf5ff;
color: #3d77b1;
font-size: 12px;
line-height: 18px;
}
.big-label a {
color: #35b4b4;
}
.project-part-sync-text {
display: inline-flex;
align-items: center;
}
.npi-jump-link {
color: #0c4dbb;
cursor: pointer;
}
.npi-jump-link:hover {
text-decoration: underline;
}
.project-part-sync-icon {
margin-left: -4px;
margin-right: 2px;
color: #1f7a35;
font-size: 10px;
transform: translateY(-1px);
}
.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;
}
.delivery-variance-alert {
color: #f56c6c;
font-weight: 700;
}
.tracking-status--unknown {
background: #f4f4f5;
}
.tracking-status--complete {
background: #f4f4f5;
color: #909399;
}
.process-cell {
background: #fff;
min-height: 44px;
padding: 4px 2px;
}
.process-cell.is-complete {
background: #ececec;
}
.process-cell.is-complete .el-input__inner {
background: #ececec;
border-color: #d5d7de;
color: #8c8c8c;
}
.process-cell.is-complete .el-input.is-disabled .el-input__inner {
background: #ececec;
color: #8c8c8c;
}
.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-readonly-content {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.process-date-display {
display: inline-block;
min-width: 74px;
text-align: center;
color: #606266;
margin-right: 9px;
}
.process-complete-btn {
padding: 4px 8px;
min-width: 12px;
}
.process-rollback-btn {
padding: 4px 6px;
min-width: 12px;
}
.process-rollback-btn--readonly {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
.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;
}
.zxClass .cell {
line-height: 24px;
font-size: 12px;
height: 24px;
}
</style>