|
|
<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: 130px" @change="handleSearchBuChange"> <el-option v-for="item in userBuList" :key="`searchBu_${item.buNo}`" :label="item.buDesc ? `${normalizeBuNo(item.buNo, searchData.site)}-${item.buDesc}` : normalizeBuNo(item.buNo, searchData.site)" :value="normalizeBuNo(item.buNo, searchData.site)" /> </el-select> </el-form-item> <el-form-item label="项目编码"> <el-input v-model="searchData.projectNo" clearable placeholder="请输入项目编码" style="width: 140px" /> </el-form-item> <el-form-item label="项目描述"> <el-input v-model="searchData.projectDesc" clearable placeholder="请输入项目描述" style="width: 160px" /> </el-form-item> <el-form-item label="项目物料"> <el-input v-model="searchData.testPartNo" clearable placeholder="请输入项目物料" style="width: 140px" /> </el-form-item> <el-form-item label="料号描述"> <el-input v-model="searchData.partDesc" clearable placeholder="请输入料号描述" style="width: 160px" /> </el-form-item> <el-form-item v-if="searchData.buNo==='03-RFID'" 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-input v-model="searchData.customerNo" clearable placeholder="请输入客户编码" style="width: 120px" /> </el-form-item> <el-form-item label="打样单号"> <el-input v-model="searchData.proofingNo" clearable placeholder="请输入打样单号" style="width: 150px" /> </el-form-item> <el-form-item label="项目/物料同步"> <el-select v-model="searchData.projectPartSyncFlag" clearable placeholder="全部" style="width: 120px"> <el-option label="已同步" value="Y" /> <el-option label="未同步" value="N" /> </el-select> </el-form-item> <el-form-item label="打样单同步"> <el-select v-model="searchData.proofSyncFlag" clearable placeholder="全部" style="width: 120px"> <el-option label="已同步" value="Y" /> <el-option label="未同步" value="N" /> </el-select> </el-form-item> <el-form-item label="打样状态"> <el-select v-model="searchData.proofingStatus" clearable placeholder="全部" style="width: 120px"> <el-option label="进行中" value="进行中" /> <el-option label="打样完成" value="打样完成" /> <el-option label="草稿" value="草稿" /> </el-select> <el-button type="primary" @click="getDataList('Y')">查询</el-button> <el-button @click="resetSearch">重置</el-button> <el-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="toggleBatchEdit">{{ batchEditMode ? '取消修改' : '批量修改' }}</el-button> <el-button v-if="batchEditMode" type="success" @click="saveBatchPlan">保存修改</el-button>--> <el-button type="warning" @click="openCreateProofDialog()">新增打样</el-button> <el-button type="success" plain :loading="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 @click="openBuProcessConfigDialog">事业部进度设置</el-button> <el-button type="info" v-if="isAuth('833001005:set2')" plain @click="openDefaultProcessConfigDialog">默认进度设置</el-button> <el-tooltip style="margin-top: 2px;margin-left: 10px" content="已完成后,双击日期选择框可回退为未完成" placement="top"> <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="700" v-loading="dataListLoading" @selection-change="selectionChange" @row-click="rowClick" style="width: 100%; margin-top: 8px" > <el-table-column type="selection" width="45" /> <el-table-column 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="isProjectPartSyncColumn(item.columnProp)" :class="getProjectPartSyncTextClass(scope.row)" >{{ getTrackingColumnDisplayValue(scope.row, item.columnProp) }}</span> <span v-else-if="item.columnProp === 'proofingNo'" :class="getProofSyncTextClass(scope.row)" >{{ getTrackingColumnDisplayValue(scope.row, item.columnProp) }}</span> <a v-else-if="item.columnProp === 'proofingStatus'" :style="{ color: (scope.row.proofingStatus === '打样完成' || 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>{{ 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}`" :label="item.label" :width="getProcessColumnWidth(item)" 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' : '']" > <el-date-picker v-if="batchEditMode" v-model="scope.row[item.actualField]" type="date" value-format="yyyy-MM-dd" format="yyyy-MM-dd" placeholder="实际完成日期" style="width: 120px" /> <div v-else :class="['process-content', isProcessStatusComplete(scope.row, item) ? 'is-complete' : '']" @mouseenter="handleProcessTooltipMouseEnter(scope.row, item, $event)" @mousemove="handleProcessTooltipMouseMove($event)" @mouseleave="handleProcessTooltipMouseLeave" > <el-date-picker v-model="scope.row[item.actualField]" type="date" value-format="yyyy-MM-dd" format="yyyy-MM-dd" placeholder="" style="width: 122px" @change="handleProcessDateChange(scope.row, item, $event)" @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> </div> </div> <div v-else class="process-cell process-cell--masked"></div> </template> </el-table-column>
<el-table-column label="Comments" width="220"> <template slot-scope="scope"> <div class="comments-cell" @dblclick.stop="startCommentsEdit(scope.row)"> <template v-if="batchEditMode"> <el-input v-model="scope.row.comments" class="comments-edit-input" size="mini" clearable placeholder="请输入Comments" /> </template> <template v-else-if="isCommentsEditing(scope.row)"> <el-input v-model="commentsDraft" v-focus-comments-input="isCommentsEditing(scope.row)" :ref="'commentsInput_' + scope.row.trackingId" :id="'commentsInput_' + scope.row.trackingId" class="comments-edit-input" size="mini" clearable placeholder="请输入,回车也可保存" @keydown.enter.native.prevent="saveCommentsEdit(scope.row)" @keyup.esc.native="cancelCommentsEdit" @blur="handleCommentsBlur(scope.row)" /> <el-button class="comments-save-btn" type="success" size="mini" plain :loading="commentsSaveLoading" @mousedown.native.prevent @click.stop="saveCommentsEdit(scope.row)" >√</el-button> </template> <span v-else class="comments-text" :title="scope.row.comments || ''">{{ scope.row.comments || '-' }}</span> </div> </template> </el-table-column> <el-table-column label="操作" fixed="right" width="150" align="center"> <template slot-scope="scope"> <a @click="openOneKeyDialog('edit', scope.row)">修改</a> <a class="action-link delete-link" @click="deleteProof(scope.row)">删除</a> <a class="action-link end-link" v-if="scope.row.proofingNo && scope.row.proofingStatus !== '打样完成'" @click="finishProof(scope.row)">打样完成</a> </template> </el-table-column> </el-table>
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalPage" layout="total, sizes, prev, pager, next, jumper" style="margin-top: 12px; text-align: right" />
<div v-show="processTooltip.visible" class="process-history-tooltip" :style="{ left: `${processTooltip.left}px`, top: `${processTooltip.top}px` }" >{{ processTooltip.content }}</div>
<el-dialog title="进度展示设置" :visible.sync="buProcessConfigDialogVisible" width="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="oneKeyDialogMode === 'edit' ? '修改项目/物料/打样' : '一键创建项目/物料/打样'" :visible.sync="oneKeyDialogVisible" :close-on-click-modal="false" width="1000px"> <el-form :model="oneKeyForm" label-position="top" class="one-key-grid-form"> <el-row :gutter="16"> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('buNo')"> <el-form-item label="BU" required> <el-select v-model="oneKeyForm.buNo" placeholder="请选择" style="width: 100%" @change="handleOneKeyBuChange"> <el-option v-for="item in userBuList" :key="item.buNo" :label="item.buDesc ? `${normalizeBuNo(item.buNo, searchData.site)}-${item.buDesc}` : normalizeBuNo(item.buNo, searchData.site)" :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="手工输入或按BU选择NPI项目" style="width: 100%" @select="handleOneKeyProjectSelect" @blur="handleOneKeyProjectNoBlur"> <template slot-scope="{ item }"> <div class="one-key-suggest-main">{{ item.value }}</div> <div class="one-key-suggest-sub"> {{ item.projectDesc || '-' }} | {{ item.customerDesc || '-' }} </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 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-main">{{ item.value }}</div> <div class="one-key-suggest-sub">{{ item.partDesc || '-' }} | 创建: {{ item.createDateText || '-' }}</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 style="width: 100%"></el-input> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('projectOwner')"> <el-form-item required> <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('projectOwner')">PjM</a></span> <el-input v-model="oneKeyForm.projectOwnerName" disabled style="width: 100%"></el-input> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('engineer')"> <el-form-item required> <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('engineer')">Engineer</a></span> <el-input v-model="oneKeyForm.engineerName" disabled style="width: 100%"></el-input> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cManufactureEngineer')"> <el-form-item> <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cManufactureEngineer')">MFG</a></span> <el-input v-model="oneKeyForm.cManufactureEngineerName" disabled style="width: 100%"></el-input> </el-form-item> </el-col> </el-row>
<el-row :gutter="16"> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer6')"> <el-form-item> <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer6')">IQC</a></span> <el-input v-model="oneKeyForm.cQualityEngineer6Name" disabled style="width: 100%"></el-input> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer1')"> <el-form-item> <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer1')">IPQC-Lam/Pri/Etch/Slit</a></span> <el-input v-model="oneKeyForm.cQualityEngineer1Name" disabled style="width: 100%"></el-input> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer2')"> <el-form-item> <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer2')">IPQC-Converting</a></span> <el-input v-model="oneKeyForm.cQualityEngineer2Name" disabled style="width: 100%"></el-input> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer4')"> <el-form-item> <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer4')">SQE</a></span> <el-input v-model="oneKeyForm.cQualityEngineer4Name" disabled style="width: 100%"></el-input> </el-form-item> </el-col> </el-row>
<el-row :gutter="16"> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer3')"> <el-form-item> <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer3')">FQC1</a></span> <el-input v-model="oneKeyForm.cQualityEngineer3Name" disabled style="width: 100%"></el-input> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer5')"> <el-form-item> <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer5')">FQC2</a></span> <el-input v-model="oneKeyForm.cQualityEngineer5Name" disabled style="width: 100%"></el-input> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('ipqcHardTag')"> <el-form-item> <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('ipqcHardTag')">IPQC-Hardtag</a></span> <el-input v-model="oneKeyForm.ipqcHardTagName" disabled style="width: 100%"></el-input> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('cQualityEngineer7')"> <el-form-item> <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('cQualityEngineer7')">前道工程师</a></span> <el-input v-model="oneKeyForm.cQualityEngineer7Name" disabled style="width: 100%"></el-input> </el-form-item> </el-col> </el-row>
<el-row :gutter="16"> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('docEngineer')"> <el-form-item> <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('docEngineer')">文档工程师</a></span> <el-input v-model="oneKeyForm.docEngineerName" disabled style="width: 100%"></el-input> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('docEngineer2')"> <el-form-item> <span slot="label" class="big-label"><a href="javascript:void(0)" @click="openRoleDialog('docEngineer2')">文档工程师2</a></span> <el-input v-model="oneKeyForm.docEngineer2Name" disabled style="width: 100%"></el-input> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('projectStatus')"> <el-form-item label="项目状态"> <el-select v-model="oneKeyForm.projectStatus" clearable style="width: 100%"> <el-option v-for="item in projectStatusList" :key="item" :label="item" :value="item" /> </el-select> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('projectPhase')"> <el-form-item label="项目阶段"> <el-select v-model="oneKeyForm.projectPhase" clearable style="width: 100%"> <el-option v-for="item in proofProjectPhaseList" :key="item.projectPhase" :label="item.projectPhase" :value="item.projectPhase" /> </el-select> </el-form-item> </el-col> </el-row>
<el-row :gutter="16"> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('proofingNo')"> <el-form-item label="打样单号"> <el-select v-model="oneKeyForm.proofingNo" placeholder="请选择打样单号" clearable filterable :loading="oneKeyProofingApplyLoading" :disabled="isBlankValue(oneKeyForm.projectNo)" style="width: 100%" @change="handleOneKeyProofingNoChange"> <el-option v-for="item in oneKeyProofingApplyOptions" :key="item.applyNo" :label="item.applyNo" :value="item.applyNo" /> </el-select> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('proofingStatus')"> <el-form-item label="打样状态"> <el-select v-model="oneKeyForm.proofingStatus" clearable style="width: 100%"> <el-option label="草稿" value="草稿" /> <el-option label="进行中" value="进行中" /> <el-option label="打样完成" value="打样完成" /> <el-option label="打样结束" value="打样结束" /> </el-select> </el-form-item> </el-col> <el-col :span="6" v-show="isOneKeyCreateFieldVisible('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('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)" 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">取消</el-button> <el-button type="primary" :loading="saveOneKeyLoading" @click="submitOneKey">{{ oneKeyDialogMode === 'edit' ? '保存修改' : '保存' }}</el-button> </el-footer> </el-dialog>
<el-dialog title="新增打样" :visible.sync="proofDialogVisible" width="768px" :close-on-click-modal="false"> <el-form :model="proofDialogData" label-position="top" class="proof-grid-form"> <el-row :gutter="16"> <el-col :span="6"> <el-form-item label="项目编码"> <el-input v-model="proofDialogData.projectNo" disabled style="width: 100%" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="项目名称"> <el-input v-model="proofDialogData.projectDesc" disabled style="width: 100%" /> </el-form-item> </el-col> <el-col :span="6"> <el-form-item label="BU"> <el-input v-model="proofDialogData.buNo" disabled style="width: 100%" /> </el-form-item> </el-col> </el-row>
<el-row :gutter="16"> <el-col :span="6"> <el-form-item label="客户编码"> <el-input v-model="proofDialogData.customerNo" disabled style="width: 100%" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="客户名称"> <el-input v-model="proofDialogData.customerDesc" disabled style="width: 100%" /> </el-form-item> </el-col> <el-col :span="6"> <el-form-item label="项目料号"> <el-input v-model="proofDialogData.testPartNo" disabled style="width: 100%" /> </el-form-item> </el-col> </el-row>
<el-row :gutter="16"> <el-col :span="12"> <el-form-item label="料号描述"> <el-input v-model="proofDialogData.partDesc" disabled style="width: 100%" /> </el-form-item> </el-col> <el-col :span="6"> <el-form-item label="项目分类" required> <el-select v-model="proofDialogData.cProjectTypeDb" placeholder="请选择" clearable style="width: 100%"> <el-option v-for="item in cProjectTypeDbList" :key="item.cProjectTypeDb" :label="item.cProjectTypeDb" :value="item.cProjectTypeDb"> </el-option> </el-select> </el-form-item> </el-col> <el-col :span="6"> <el-form-item label="项目阶段" required> <el-select v-model="proofDialogData.projectPhase" placeholder="请选择" clearable style="width: 100%"> <el-option v-for="item in proofProjectPhaseList" :key="item.projectPhase" :label="item.projectPhase" :value="item.projectPhase"> </el-option> </el-select> </el-form-item> </el-col> </el-row>
<el-row :gutter="16"> <el-col :span="6"> <el-form-item label="打样单号" required> <el-select v-model="proofDialogData.proofingNo" placeholder="请选择试验单号" clearable filterable :loading="proofDialogApplyLoading" :disabled="isBlankValue(proofDialogData.projectNo)" style="width: 100%" @change="handleProofDialogProofingNoChange"> <el-option v-for="item in proofDialogApplyOptions" :key="item.applyNo" :label="item.applyNo" :value="item.applyNo" /> </el-select> </el-form-item> </el-col> <el-col :span="6"> <el-form-item label="打样开始日期" required> <el-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="12"> <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="processDialogVisible" width="620px" :close-on-click-modal="false"> <el-form :model="processForm" label-position="top" label-width="120px" :inline="true"> <el-form-item label="工序"> <el-input v-model="processForm.processName" disabled style="width: 180px" /> </el-form-item> <el-form-item label="状态" required> <el-select v-model="processForm.status" style="width: 180px"> <el-option label="未完成" value="未完成" /> <el-option label="进行中" value="进行中" /> <el-option label="已完成" value="已完成" /> </el-select> </el-form-item> <el-form-item label="实际完成日期"> <el-date-picker v-model="processForm.actualDate" type="date" value-format="yyyy-MM-dd" format="yyyy-MM-dd" style="width: 180px" /> </el-form-item> <el-form-item label="备注"> <el-input type="textarea" :rows="2" v-model="processForm.remark" style="width: 570px" /> </el-form-item> </el-form>
<el-table :data="processHistoryRows" border stripe height="220" style="margin-top: 32px"> <el-table-column prop="changedDate" label="修改时间" width="120" /> <el-table-column prop="changedBy" label="修改人" width="70" /> <el-table-column prop="oldStatus" label="原状态" width="60" /> <el-table-column prop="newStatus" label="新状态" width="60" /> <el-table-column prop="oldActualDate" label="原实际" width="90" /> <el-table-column prop="newActualDate" label="新实际" width="90" /> <el-table-column prop="remark" label="备注" show-overflow-tooltip /> </el-table>
<div slot="footer"> <el-button @click="processDialogVisible = false">取消</el-button> <el-button type="primary" :loading="saveProcessLoading" @click="submitProcessUpdate">保存</el-button> </div> </el-dialog>
<el-dialog title="打样完成" :visible.sync="finishDialogVisible" width="260px" :close-on-click-modal="false" custom-class="finish-proof-dialog"> <div class="finish-dialog-tip"> <i class="el-icon-info"></i> <span>确认后将直接标记为<strong class="finish-tip-highlight">打样完成</strong></span> </div> <el-form :model="finishForm" label-width="100px" class="finish-form"> <el-form-item label="实际完成日期" required> <el-date-picker v-model="finishForm.actualityDeliveryDate" type="date" value-format="yyyy-MM-dd" format="yyyy-MM-dd" placeholder="选择日期" style="width: 100%" /> </el-form-item> </el-form> <div slot="footer"> <el-button @click="finishDialogVisible = false">取消</el-button> <el-button type="primary" :loading="finishSaveLoading" @click="submitFinishProof">确认完成</el-button> </div> </el-dialog>
<el-dialog :title="roleDialogTitle" :visible.sync="roleDialogVisible" width="559px" :close-on-click-modal="false"> <el-form :inline="true" label-position="top" :model="roleSearchData" style="margin-left: 7px; margin-top: -5px;"> <el-form-item label="用户账号"> <el-input v-model="roleSearchData.username" clearable style="width: 110px"></el-input> </el-form-item> <el-form-item label="用户名"> <el-input v-model="roleSearchData.userDisplay" clearable style="width: 110px"></el-input> </el-form-item> <el-form-item label="是否在用"> <el-select filterable v-model="roleSearchData.active" clearable style="width: 140px"> <el-option label="是" value="Y"></el-option> <el-option label="否" value="N"></el-option> </el-select> </el-form-item> <el-form-item label=" "> <el-button type="primary" style="padding: 3px 12px" @click="queryRoleDialogList">查询</el-button> </el-form-item> </el-form> <el-table :data="roleDialogList" stripe border highlight-current-row @row-dblclick="pickRole" style="width: 100%"> <el-table-column prop="username" header-align="center" align="center" label="用户账号"></el-table-column> <el-table-column prop="userDisplay" header-align="center" align="center" label="用户名"></el-table-column> <el-table-column prop="active" header-align="center" align="center" label="是否在用"></el-table-column> </el-table> <el-footer style="height: 40px; margin-top: 10px; text-align: center"> <el-button @click="roleDialogVisible = false">关闭</el-button> </el-footer> </el-dialog>
<el-dialog title="新客户" :visible.sync="newCustomerFlag" width="348px" :close-on-click-modal="false" @close="closeNewCustomer"> <el-form :inline="true" label-position="top" :model="newCustomerData" style="margin-left: 7px; margin-top: -5px;"> <el-form-item label="客户名称"> <el-input v-model="newCustomerData.customerDesc" style="width: 313px"></el-input> </el-form-item> </el-form> <el-footer style="height: 40px; margin-top: 10px; text-align: center"> <el-button type="primary" @click="saveNewCustomer">保存</el-button> <el-button @click="newCustomerFlag = false">关闭</el-button> </el-footer> </el-dialog>
<Chooselist ref="baseList" @getBaseData="getBaseData"></Chooselist> </div></template>
<script>import { batchUpdateProofTrackingPlan, createProofTrackingRecord, deleteProofTracking, exportProofTracking, finishProofTracking, oneKeyCreateProofTracking, oneKeyUpdateProofTracking, queryProofTrackingBuProcessConfig, queryProofTrackingProcessColumns, queryProofTrackingProcessCategories, deleteProofTrackingProcessColumnsManage, queryProofTrackingProcessColumnsManage, queryProofTrackingProcessHistory, saveProofTrackingBuProcessConfig, saveProofTrackingProcessCategoriesManage, saveProofTrackingProcessColumnsManage, searchProjectInfoTracking, searchProjectPartTracking, searchProofTracking, 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 }, { 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: '', projectPartSyncFlag: '', proofSyncFlag: '', proofingStatus: '' }, trackingColumnList: cloneDefaultTrackingColumns(), hasUserTrackingColumnConfig: false, processColumns: cloneDefaultProcessColumns(), processCodeLabelMap: buildProcessCodeLabelMapByRows(cloneDefaultProcessColumns()), buProcessCategoryOptionsMap: {}, 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', 'requiredDeliveryDate', 'remark' ], userBuList: [], tagNo: '', newCustomerFlag: false, newCustomerData: { customerDesc: '' }, roleDialogVisible: false, roleDialogTitle: '', roleDialogField: '', roleDialogApiKey: '0', roleDialogList: [], roleSearchData: { site: this.$store.state.user.site, username: '', userDisplay: '', active: '', page: 1, limit: 50 }, roleConfig: { projectManager: { apiKey: '0', title: 'PM/Sales', nameField: 'projectManagerName' }, projectOwner: { apiKey: '1', title: 'PjM', nameField: 'projectOwnerName' }, cQualityEngineer1: { apiKey: '2', title: 'IPQC-Lam/Pri/Etch/Slit', nameField: 'cQualityEngineer1Name' }, cQualityEngineer2: { apiKey: '3', title: 'IPQC-Converting', nameField: 'cQualityEngineer2Name' }, cQualityEngineer3: { apiKey: '4', title: 'FQC1', nameField: 'cQualityEngineer3Name' }, cManufactureEngineer: { apiKey: '5', title: 'MFG', nameField: 'cManufactureEngineerName' }, engineer: { apiKey: '6', title: 'Engineer', nameField: 'engineerName' }, cQualityEngineer4: { apiKey: '7', title: 'SQE', nameField: 'cQualityEngineer4Name' }, cQualityEngineer5: { apiKey: '7', title: 'FQC2', nameField: 'cQualityEngineer5Name' }, cQualityEngineer6: { apiKey: '7', title: 'IQC', nameField: 'cQualityEngineer6Name' }, docEngineer: { apiKey: '7', title: '文档工程师', nameField: 'docEngineerName' }, ipqcHardTag: { apiKey: '7', title: 'IPQC-Hardtag', nameField: 'ipqcHardTagName' }, cQualityEngineer7: { apiKey: '7', title: '前道工程师', nameField: 'cQualityEngineer7Name' }, docEngineer2: { apiKey: '8', title: '文档工程师2', nameField: 'docEngineer2Name' } }, dataList: [], dataListLoading: false, exportLoading: false, syncBatchNpiLoading: false, syncBatchProofLoading: false, pageIndex: 1, pageSize: 20, totalPage: 0, currentRow: null, manualSelectedTrackingId: null, selectionRows: [], batchEditMode: false, processHistoryMap: {}, processTooltipHoverKey: '', processTooltip: { visible: false, content: '', left: 0, top: 0 }, processHistoryRows: [], commentsEditingTrackingId: null, commentsDraft: '', commentsSaveLoading: false,
oneKeyDialogVisible: false, oneKeyDialogMode: 'create', saveOneKeyLoading: false, oneKeyForm: {}, oneKeyProofingApplyOptions: [], oneKeyProofingApplyLoading: false, oneKeyProofingApplyQueryToken: 0, oneKeyProjectQueryToken: 0, oneKeyPartQueryToken: 0, oneKeyProjectNoSnapshot: '', oneKeyProjectRoleBaseline: {},
proofDialogVisible: false, proofSaveLoading: false, proofDialogData: this.getDefaultProofDialogData(), proofDialogApplyOptions: [], proofDialogApplyLoading: false, proofDialogApplyQueryToken: 0,
processDialogVisible: false, saveProcessLoading: false, processForm: {},
finishDialogVisible: false, finishSaveLoading: false, finishForm: { trackingId: null, actualityDeliveryDate: '' } } }, activated () { this.loadUserBuList().then(() => { return Promise.all([this.queryProcessColumns(), this.loadTrackingColumns()]) }).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 }, 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 } }, 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, false) 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 } return this.normalizeTrackingColumnBoolean(column.columnHidden, false) === false }, isProjectPartSyncColumn (columnProp) { return ['projectNo', 'projectDesc', 'testPartNo', 'partDesc'].indexOf(columnProp) > -1 }, getTrackingColumnDisplayValue (row, columnProp) { if (!row || !columnProp) { return '' } const value = row[columnProp] return this.isBlankValue(value) ? '' : String(value) }, 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) }, handleSearchBuChange () { this.ensureSearchBuNoSelected() // 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 () { this.ensureBuProcessConfigRows() this.processOrderDragState = { buNo: '', fromIndex: -1 } this.buProcessConfigDialogVisible = true this.loadAllBuProcessCategoryOptions(this.buProcessConfigRows, 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: 200 } return apiFn(params).then(({ data }) => { if (data && data.code === 0) { return (data.rows || []).map(item => { return { username: item.username, userDisplay: item.userDisplay || item.user_display || '', active: item.active } }) } return [] }) }, newCustomer () { this.newCustomerData.customerDesc = '' this.newCustomerFlag = true }, getBaseList (val) { this.tagNo = val this.$nextTick(() => { const strVal = val === 509 ? (this.oneKeyForm.customerNo || '') : '' this.$refs.baseList.init(val, strVal, '') }) }, getBaseData (val) { if (this.tagNo === 509) { this.oneKeyForm.customerNo = val.customer_no this.oneKeyForm.customerDesc = val.customer_desc } }, customerNoBlur () { if (!this.oneKeyForm.customerNo) { this.oneKeyForm.customerDesc = '' return } const params = { customerNo: this.oneKeyForm.customerNo, createBy: this.$store.state.user.name } queryCustomerList(params).then(({ data }) => { if (data && data.code === 0 && data.rows && data.rows.length === 1) { this.oneKeyForm.customerDesc = data.rows[0].customerDesc } else { this.oneKeyForm.customerDesc = '' } }) }, saveNewCustomer () { if (!this.newCustomerData.customerDesc) { this.$message.warning('请输入客户名称!') return } this.generateNextCustomerNo().then(customerNo => { if (!customerNo) { this.$alert('该客户已存在,请重新输入客户名称!', '提示', { confirmButtonText: '确定', type: 'warning' }) return } const params = { site: this.$store.state.user.site, customerNo: customerNo, customerDesc: this.newCustomerData.customerDesc, createBy: this.$store.state.user.name } saveNewCustomer(params).then(({ data }) => { if (data && data.code === 0) { this.oneKeyForm.customerNo = customerNo this.oneKeyForm.customerDesc = this.newCustomerData.customerDesc this.newCustomerFlag = false this.$message.success(data.msg || '新增客户成功') } else { this.$alert(data && data.msg ? data.msg : '新增客户失败', '提示', { confirmButtonText: '确定', type: 'warning' }) } }) }) }, generateNextCustomerNo () { const params = { site: this.$store.state.user.site, customerDesc: this.newCustomerData.customerDesc, createBy: this.$store.state.user.name } return new Promise(resolve => { getCustomerNo(params).then(({ data }) => { if (!(data && data.code === 0)) { resolve(false) return } if (!data.data || !data.data.customerNo || data.data === '0') { resolve('C0001') return } const lastCustomerNo = parseInt(String(data.data.customerNo).substring(1), 10) || 0 const nextCustomerNo = lastCustomerNo + 1 if (nextCustomerNo < 10) { resolve('C000' + nextCustomerNo) } else if (nextCustomerNo < 100) { resolve('C00' + nextCustomerNo) } else if (nextCustomerNo < 1000) { resolve('C0' + nextCustomerNo) } else { resolve('C' + nextCustomerNo) } }).catch(() => { resolve(false) }) }) }, closeNewCustomer () { this.newCustomerData.customerDesc = '' this.newCustomerFlag = false }, openRoleDialog (field) { const roleCfg = this.roleConfig[field] if (!roleCfg) { return } this.roleDialogField = field this.roleDialogApiKey = roleCfg.apiKey this.roleDialogTitle = roleCfg.title this.roleSearchData = { site: this.$store.state.user.site, username: '', userDisplay: '', active: '', page: 1, limit: 50 } this.roleDialogVisible = true this.queryRoleDialogList() }, queryRoleDialogList () { this.fetchRoleListByApiKey(this.roleDialogApiKey, this.roleSearchData).then(rows => { this.roleDialogList = rows }) }, pickRole (row) { const roleCfg = this.roleConfig[this.roleDialogField] if (!roleCfg) { return } const value = row.username + '-' + row.userDisplay this.oneKeyForm[this.roleDialogField] = value this.oneKeyForm[roleCfg.nameField] = row.userDisplay this.roleDialogVisible = false }, getEmptyOneKeyForm () { return { site: this.$store.state.user.site, buNo: '', projectNo: '', projectName: '', projectDesc: '', projectStatus: '草稿', projectSource: '', testPartNo: '', partDesc: '', partName: '', partSpec: '', materialNumber: '', finalPartDesc: '', finalPartNo: '', baseNo: '', revNo: '', customerNo: '', customerDesc: '', finalCustomerId: '', customerRemark: '', parentProjectNo: '', oriProjectId: '', projectCategory: '', cProjectRegion: '', projectManager: '', projectManagerName: '', projectOwner: '', projectOwnerName: '', cQualityEngineer1: '', cQualityEngineer1Name: '', cQualityEngineer2: '', cQualityEngineer2Name: '', cQualityEngineer3: '', cQualityEngineer3Name: '', cQualityEngineer4: '', cQualityEngineer4Name: '', cQualityEngineer5: '', cQualityEngineer5Name: '', cQualityEngineer6: '', cQualityEngineer6Name: '', cManufactureEngineer: '', cManufactureEngineerName: '', docEngineer: '', docEngineerName: '', docEngineer2: '', docEngineer2Name: '', ipqcHardTag: '', ipqcHardTagName: '', cQualityEngineer7: '', cQualityEngineer7Name: '', partType: '', dry_wet_type: '', partStatus: '草稿', projectPhase: '', tracker: '', engineer: '', engineerName: '', priorityLevel: '', proofingNo: '', proofingStatus: '草稿', proofingNumber: '', projectCreationDate: '', projectCloseDate: '', buildDate: '', closeDate: '', comments: '', planStartDate: '', requiredDeliveryDate: '', baseline: '', needDate: '', remark: '' } }, getDefaultProofDialogData () { return { trackingId: null, site: this.$store.state.user.site, projectId: null, projectNo: '', projectDesc: '', buNo: '', customerNo: '', customerDesc: '', projectPartId: null, testPartNo: '', partDesc: '', projectCategory: '', cProjectTypeDb: '', projectPhase: '', projectManager: '', projectOwner: '', engineer: '', priorityLevel: '', proofingNo: '', proofingNumber: '', planStartDate: '', requiredDeliveryDate: '', actualityDeliveryDate: '', proofingStatus: '草稿', remark: '', createBy: this.$store.state.user.name, updateBy: this.$store.state.user.name } }, getDataList (flag) { if (flag === 'Y') { this.pageIndex = 1 } this.searchData.site = this.$store.state.user.site this.searchData.userName = this.$store.state.user.name const inData = Object.assign({}, this.searchData, { page: this.pageIndex, limit: this.pageSize }) inData.buNo = this.normalizeBuNo(inData.buNo, inData.site) this.dataListLoading = true searchProofTracking(inData).then(({ data }) => { this.dataListLoading = false this.resetProcessTooltip() if (data && data.code === 0) { this.dataList = (data.page && data.page.list) || [] this.syncDataListProcessValues() this.totalPage = (data.page && data.page.totalCount) || 0 if (this.commentsEditingTrackingId) { const hasEditingRow = this.dataList.some(item => item.trackingId === this.commentsEditingTrackingId) if (!hasEditingRow) { this.cancelCommentsEdit() } } const manualSelectedId = this.manualSelectedTrackingId const previousCurrentId = this.currentRow && this.currentRow.trackingId let nextCurrentRow = null if (manualSelectedId != null) { // 只要用户没有手动点新行,刷新后始终尝试回选上次手动选中的那一行
nextCurrentRow = this.dataList.find(item => item && item.trackingId === manualSelectedId) || null } else if (previousCurrentId != null) { nextCurrentRow = this.dataList.find(item => item && item.trackingId === previousCurrentId) || null } if (!nextCurrentRow && manualSelectedId == null && this.dataList.length > 0) { nextCurrentRow = this.dataList[0] } this.currentRow = nextCurrentRow this.$nextTick(() => { if (this.$refs.trackingTable && typeof this.$refs.trackingTable.setCurrentRow === 'function') { this.$refs.trackingTable.setCurrentRow(this.currentRow || null) } }) } else { this.dataList = [] this.totalPage = 0 this.currentRow = null this.$nextTick(() => { if (this.$refs.trackingTable && typeof this.$refs.trackingTable.setCurrentRow === 'function') { this.$refs.trackingTable.setCurrentRow(null) } }) this.$message.error(data.msg || '查询失败') } }).catch(() => { this.dataListLoading = false this.resetProcessTooltip() this.$message.error('查询异常') }) }, 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 inData = Object.assign({}, this.searchData, { buNo: this.normalizeBuNo(this.searchData.buNo, this.searchData.site), exportTrackingColumns: exportTrackingColumns, exportProcessColumns: exportProcessColumns, exportCommentsColumn: true }) 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.projectPartSyncFlag = '' this.searchData.proofSyncFlag = '' this.searchData.proofingStatus = '' this.ensureSearchBuNoSelected() this.queryProcessColumns().then(() => { this.getDataList('Y') }) }, selectionChange (rows) { this.selectionRows = rows || [] }, isCommentsEditing (row) { return !!row && row.trackingId === this.commentsEditingTrackingId }, startCommentsEdit (row) { if (this.batchEditMode || !row || !row.trackingId || this.commentsSaveLoading) { return } this.commentsEditingTrackingId = row.trackingId this.commentsDraft = row.comments || '' }, cancelCommentsEdit () { if (this.commentsSaveLoading) { return } this.commentsEditingTrackingId = null this.commentsDraft = '' }, handleCommentsBlur (row) { if (this.commentsSaveLoading || !this.isCommentsEditing(row)) { return } const comments = this.commentsDraft == null ? '' : String(this.commentsDraft) if (comments.trim()) { this.saveCommentsEdit(row, { fromBlur: true, draftComments: comments }) return } this.cancelCommentsEdit() }, saveCommentsEdit (row, options) { const saveOptions = options || {} const fromBlur = !!saveOptions.fromBlur if (this.commentsSaveLoading) { return } if (!row || !row.trackingId) { this.$message.warning('跟踪记录ID不能为空') return } const comments = Object.prototype.hasOwnProperty.call(saveOptions, 'draftComments') ? (saveOptions.draftComments == null ? '' : String(saveOptions.draftComments)) : (this.commentsDraft == null ? '' : String(this.commentsDraft)) if (comments === (row.comments || '')) { this.cancelCommentsEdit() return } if (fromBlur) { this.commentsEditingTrackingId = null this.commentsDraft = '' } this.commentsSaveLoading = true updateProofTrackingComments({ trackingId: row.trackingId, comments: comments, updateBy: this.$store.state.user.name }).then(({ data }) => { this.commentsSaveLoading = false if (data && data.code === 0) { row.comments = comments this.$message.success('Comments保存成功') this.cancelCommentsEdit() } else { this.$message.error(data.msg || 'Comments保存失败') } }).catch(() => { this.commentsSaveLoading = false this.$message.error('Comments保存异常') }) }, rowClick (row) { this.currentRow = row this.manualSelectedTrackingId = row && row.trackingId ? row.trackingId : null }, sizeChangeHandle (val) { this.pageSize = val this.pageIndex = 1 this.getDataList() }, currentChangeHandle (val) { this.pageIndex = val this.getDataList() }, getActionRow (row) { return row || this.currentRow || (this.selectionRows.length > 0 ? this.selectionRows[0] : null) }, getTrackingProjectPartId (row) { if (!row) { return null } return row.projectPartId || row.project_part_id || null }, isSyncedByIdRule (idValue) { if (idValue == null || String(idValue).trim() === '') { return false } // 约定:NPI 正式库主键 < 100000,tracking 临时主键 >= 100000。
const numericId = Number(idValue) return Number.isFinite(numericId) && numericId > 0 && numericId < 100000 }, isProjectPartSynced (row) { if (!row) { return false } const projectId = row.projectId || row.project_id || null const projectPartId = this.getTrackingProjectPartId(row) return this.isSyncedByIdRule(projectId) && this.isSyncedByIdRule(projectPartId) }, isProofSynced (row) { if (!row) { return false } const proofingId = row.proofingId || row.proofing_id || row.trackingId || null return this.isSyncedByIdRule(proofingId) }, getProjectPartSyncTextClass (row) { return this.isProjectPartSynced(row) ? 'sync-text--ok' : 'sync-text--pending' }, getProofSyncTextClass (row) { return this.isProofSynced(row) ? 'sync-text--ok' : 'sync-text--pending' }, getProjectCategoryValue (source) { if (!source) { return '' } const category = source.cProjectTypeDb || source.projectCategory || source.project_category || source.c_project_type_db || '' return this.isBlankValue(category) ? '' : String(category).trim() }, isOneKeyCreateFieldVisible (fieldKey) { if (this.oneKeyDialogMode !== 'create') { return true } return this.oneKeyCreateVisibleFieldKeys.indexOf(fieldKey) > -1 }, isBlankValue (value) { return value == null || String(value).trim() === '' }, getApplyQuantityDisplayText (applyQuantity) { if (this.isBlankValue(applyQuantity)) { return '未维护' } return String(applyQuantity).trim() }, normalizeProjectNo (projectNo) { if (this.isBlankValue(projectNo)) { return '' } return String(projectNo).trim() }, queryApplyOptionsByProjectNo (projectNo) { const normalizedProjectNo = this.normalizeProjectNo(projectNo) if (!normalizedProjectNo) { return Promise.resolve([]) } return searchExpApplyList({ projectNo: normalizedProjectNo, page: 1, limit: 500 }).then(({ data }) => { if (!(data && data.code === 0)) { throw new Error((data && data.msg) || '查询试验单失败') } const pageList = (data.page && data.page.list) || [] const matchedProjectNo = normalizedProjectNo.toUpperCase() const optionMap = {} const optionRows = [] pageList.forEach(item => { if (!item || this.isBlankValue(item.applyNo)) { return } const applyStatus = this.isBlankValue(item.status) ? '' : String(item.status).trim() // 业务要求:已完成的试验单不允许继续作为 tracking 打样单号使用。
if (applyStatus === '草稿' || applyStatus === '已完成') { return } const rowProjectNo = this.normalizeProjectNo(item.projectNo).toUpperCase() // searchExpApplyList 按模糊 project_no 查询,这里再次做等值过滤,确保与 tracking 项目编码一一对应。
if (rowProjectNo !== matchedProjectNo) { return } const applyNo = String(item.applyNo).trim() if (optionMap[applyNo]) { return } optionMap[applyNo] = true optionRows.push({ applyNo: applyNo, expectedFinishDate: this.formatDate(item.expectedFinishDate), 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.oneKeyProjectNoSnapshot = '' this.oneKeyForm.testPartNo = '' this.oneKeyForm.partDesc = '' this.oneKeyForm.partType = '' this.oneKeyForm.dry_wet_type = '' return } if (previousProjectNo.toUpperCase() !== projectNo.toUpperCase()) { // 项目编码变化后需清理旧料号,避免跨项目保留旧料号与角色信息。
this.oneKeyForm.testPartNo = '' this.oneKeyForm.partDesc = '' this.oneKeyForm.partType = '' this.oneKeyForm.dry_wet_type = '' } this.oneKeyProjectNoSnapshot = projectNo this.loadOneKeyProofingApplyOptions(projectNo, { keepCurrentIfMissing: false }) }, handleOneKeyProofingNoChange (proofingNo) { const selectedNo = this.isBlankValue(proofingNo) ? '' : String(proofingNo).trim() if (this.oneKeyDialogMode === 'edit') { // 修改模式下预计完成日期以数据库已存值为准,不跟随打样单号自动回填/清空。
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 = '' 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.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), applyQuantity: '' }) } if (!hasCurrent && !keepCurrentIfMissing) { this.oneKeyForm.proofingNo = '' this.oneKeyForm.requiredDeliveryDate = '' } } this.oneKeyProofingApplyOptions = nextOptions if (currentProofingNo) { this.handleOneKeyProofingNoChange(currentProofingNo) } return nextOptions }).catch((e) => { if (queryToken === this.oneKeyProofingApplyQueryToken) { this.oneKeyProofingApplyOptions = [] this.$message.error((e && e.message) || '查询试验单异常') } return [] }).finally(() => { if (queryToken === this.oneKeyProofingApplyQueryToken) { this.oneKeyProofingApplyLoading = false } }) }, handleProofDialogProofingNoChange (proofingNo) { const selectedNo = this.isBlankValue(proofingNo) ? '' : String(proofingNo).trim() if (!selectedNo) { this.proofDialogData.requiredDeliveryDate = '' return } const matched = this.proofDialogApplyOptions.find(item => item && item.applyNo === selectedNo) if (!matched) { return } this.proofDialogData.proofingNo = matched.applyNo this.proofDialogData.requiredDeliveryDate = matched.expectedFinishDate || '' }, loadProofDialogApplyOptions (projectNo) { const normalizedProjectNo = this.normalizeProjectNo(projectNo) const queryToken = this.proofDialogApplyQueryToken + 1 this.proofDialogApplyQueryToken = queryToken if (!normalizedProjectNo) { this.proofDialogApplyLoading = false this.proofDialogApplyOptions = [] this.proofDialogData.proofingNo = '' this.proofDialogData.requiredDeliveryDate = '' return Promise.resolve([]) } this.proofDialogApplyLoading = true return this.queryApplyOptionsByProjectNo(normalizedProjectNo).then(optionRows => { if (queryToken !== this.proofDialogApplyQueryToken) { return optionRows } this.proofDialogApplyOptions = optionRows const currentProofingNo = this.isBlankValue(this.proofDialogData.proofingNo) ? '' : String(this.proofDialogData.proofingNo).trim() if (currentProofingNo) { this.handleProofDialogProofingNoChange(currentProofingNo) } return optionRows }).catch((e) => { if (queryToken === this.proofDialogApplyQueryToken) { this.proofDialogApplyOptions = [] this.$message.error((e && e.message) || '查询试验单异常') } return [] }).finally(() => { if (queryToken === this.proofDialogApplyQueryToken) { this.proofDialogApplyLoading = false } }) }, getBuOptionValue (site, buNo) { const rawBuNo = buNo == null ? '' : String(buNo).trim() if (!rawBuNo) { return '' } if (rawBuNo.indexOf('_') > -1) { return rawBuNo } const targetSuffix = `_${rawBuNo}` const matched = this.userBuList.find(item => item && item.buNo && (item.buNo === rawBuNo || item.buNo === `${site}_${rawBuNo}` || String(item.buNo).endsWith(targetSuffix))) if (matched && matched.buNo) { return matched.buNo } return site ? `${site}_${rawBuNo}` : rawBuNo }, getRoleDisplayName (value) { if (this.isBlankValue(value)) { return '' } const strVal = String(value) const splitIdx = strVal.indexOf('-') return splitIdx > -1 ? strVal.substring(splitIdx + 1) : strVal }, 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) }) }, queryOneKeyProjectOptions (projectNoKeyword) { const buContext = this.getOneKeyBuQueryContext() if (!buContext) { return Promise.resolve([]) } const queryKeyword = this.isBlankValue(projectNoKeyword) ? '' : String(projectNoKeyword).trim() const inData = { site: buContext.site, userName: this.$store.state.user.name, buDesc: buContext.buDesc, projectNo: queryKeyword, projectDesc: '', 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 normalizedKeyword = queryKeyword.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 } if (!normalizedKeyword) { return true } return String(item.projectNo).toUpperCase().indexOf(normalizedKeyword) > -1 }) return this.sortRowsByDateDesc(filteredRows, ['createDate', 'projectCreationDate', 'updateDate']) }).catch(() => { return [] }) }, queryOneKeyPartOptions (projectNo, partNoKeyword) { const normalizedProjectNo = this.normalizeProjectNo(projectNo) if (!normalizedProjectNo) { return Promise.resolve([]) } const buContext = this.getOneKeyBuQueryContext() const queryKeyword = this.isBlankValue(partNoKeyword) ? '' : String(partNoKeyword).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: queryKeyword, partDesc: '', 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 normalizedKeyword = queryKeyword.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 } if (!normalizedKeyword) { return true } return rowPartNo.toUpperCase().indexOf(normalizedKeyword) > -1 }) return this.sortRowsByDateDesc(filteredRows, ['createDate', 'updateDate', 'buildDate']) }).catch(() => { return [] }) }, queryOneKeyProjectSuggestions (queryString, callback) { const queryToken = this.oneKeyProjectQueryToken + 1 this.oneKeyProjectQueryToken = queryToken this.queryOneKeyProjectOptions(queryString).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.oneKeyForm.projectNo) if (!projectNo) { callback([]) return } const queryToken = this.oneKeyPartQueryToken + 1 this.oneKeyPartQueryToken = queryToken this.queryOneKeyPartOptions(projectNo, queryString).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.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 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 } 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 || '', 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' : 'create' this.oneKeyDialogMode = dialogMode if (dialogMode !== 'edit') { this.oneKeyProofingApplyQueryToken += 1 this.oneKeyProofingApplyLoading = false this.oneKeyProofingApplyOptions = [] this.oneKeyProjectQueryToken += 1 this.oneKeyPartQueryToken += 1 this.oneKeyProjectNoSnapshot = '' 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('请先选择一条记录后再修改项目/物料/打样') this.oneKeyDialogMode = 'create' return } const projectPartId = this.getTrackingProjectPartId(current) if (!current.projectId || !projectPartId || !current.trackingId) { this.$message.warning('当前记录缺少项目/物料/打样关键信息,无法修改') this.oneKeyDialogMode = 'create' return } const queryUser = this.$store.state.user.name const querySite = current.site || this.$store.state.user.site this.saveOneKeyLoading = true Promise.all([ searchProjectInfoTracking({ site: querySite, userName: queryUser, projectId: current.projectId, page: 1, limit: 1 }), searchProjectPartTracking({ site: querySite, userName: queryUser, projectPartId: projectPartId, page: 1, limit: 1 }) ]).then(([projectResp, partResp]) => { const projectData = projectResp && projectResp.data const partData = partResp && partResp.data if (!projectData || projectData.code !== 0) { throw new Error((projectData && projectData.msg) || '加载项目信息失败') } if (!partData || partData.code !== 0) { throw new Error((partData && partData.msg) || '加载项目物料信息失败') } const projectRows = (projectData.page && projectData.page.list) || [] const partRows = (partData.page && partData.page.list) || [] if (projectRows.length === 0) { throw new Error('未找到对应的项目信息,无法修改') } if (partRows.length === 0) { throw new Error('未找到对应的项目物料信息,无法修改') } this.oneKeyForm = this.buildOneKeyEditForm(current, projectRows[0], partRows[0]) this.snapshotOneKeyProjectRoles(projectRows[0]) this.oneKeyProjectNoSnapshot = this.normalizeProjectNo(this.oneKeyForm.projectNo) 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.proofingStatus = '' this.getDataList('Y') } else { this.$message.error(data.msg || '一键创建失败') } }).catch(() => { this.saveOneKeyLoading = false this.$message.error('一键创建异常') }) }, openCreateProofDialog (row) { const current = this.getActionRow(row) if (!current) { this.$message.warning('请先选择一条记录') return } this.currentRow = current const projectPartId = this.getTrackingProjectPartId(current) || current.id const projectCategory = this.getProjectCategoryValue(current) this.proofDialogApplyQueryToken += 1 this.proofDialogApplyLoading = false this.proofDialogApplyOptions = [] this.proofDialogData = Object.assign(this.getDefaultProofDialogData(), { trackingId: null, site: current.site || this.$store.state.user.site, projectId: current.projectId, projectNo: current.projectNo, projectDesc: current.projectDesc, buNo: current.buNo, customerNo: current.customerNo, customerDesc: current.customerDesc, projectPartId: projectPartId, testPartNo: current.testPartNo, partDesc: current.partDesc, projectCategory: projectCategory, cProjectTypeDb: projectCategory, projectManager: current.projectManager, projectOwner: current.projectOwner, engineer: current.engineer, priorityLevel: current.priority, proofingNo: '', proofingNumber: '', planStartDate: '', requiredDeliveryDate: '', actualityDeliveryDate: '', proofingStatus: '草稿', remark: '', createBy: this.$store.state.user.name, updateBy: this.$store.state.user.name }) if (this.isBlankValue(projectCategory)) { this.$message.warning('未获取到项目分类,请手动选择') } this.proofDialogVisible = true this.loadProofDialogApplyOptions(this.proofDialogData.projectNo) }, 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, proofingStatus: this.proofDialogData.proofingStatus || '草稿', remark: this.proofDialogData.remark, tracker: this.proofDialogData.projectOwner, engineer: this.proofDialogData.engineer, priorityLevel: this.proofDialogData.priorityLevel, createBy: this.$store.state.user.name, updateBy: this.$store.state.user.name } }, saveProofRecord () { if (!this.validateProofDialogData()) { return } const payload = this.buildProofPayload() this.proofSaveLoading = true createProofTrackingRecord(payload).then(({ data }) => { this.proofSaveLoading = false if (data && data.code === 0) { this.$message.success(data.msg || '新增打样成功') this.proofDialogVisible = false this.getDataList() } else { this.$alert(data && data.msg ? data.msg : '新增打样失败', '错误', { confirmButtonText: '确定' }) } }).catch(() => { this.proofSaveLoading = false this.$message.error('新增打样异常') }) }, toggleBatchEdit () { this.batchEditMode = !this.batchEditMode if (this.batchEditMode) { this.cancelCommentsEdit() } this.$nextTick(() => { const tableRef = this.$refs.trackingTable if (tableRef && tableRef.doLayout) { tableRef.doLayout() } if (this.batchEditMode) { setTimeout(() => { this.scrollToRawMaterialColumn() }, 0) } }) }, scrollToRawMaterialColumn () { const tableRef = this.$refs.trackingTable if (!tableRef || !tableRef.$el) { return } const headerWrapper = tableRef.headerWrapper || tableRef.$el.querySelector('.el-table__header-wrapper') const bodyWrapper = tableRef.bodyWrapper || tableRef.$el.querySelector('.el-table__body-wrapper') if (!headerWrapper || !bodyWrapper) { return } const headerCells = headerWrapper.querySelectorAll('th') let targetCell = null for (let i = 0; i < headerCells.length; i++) { const text = (headerCells[i].innerText || '').replace(/\s+/g, ' ').trim() if (text === 'Raw Material') { targetCell = headerCells[i] break } } if (!targetCell) { return } const fixedLeftWrapper = tableRef.$el.querySelector('.el-table__fixed') const fixedLeftWidth = fixedLeftWrapper ? fixedLeftWrapper.offsetWidth : 0 const nextScrollLeft = Math.max(targetCell.offsetLeft - fixedLeftWidth - 8, 0) bodyWrapper.scrollLeft = nextScrollLeft headerWrapper.scrollLeft = nextScrollLeft }, saveBatchPlan () { const rows = this.selectionRows && this.selectionRows.length > 0 ? this.selectionRows : this.dataList if (!rows || rows.length === 0) { this.$message.warning('没有可保存的数据') return } const payloadRows = rows.map(row => ({ trackingId: row.trackingId, rawMaterialActualDate: row.rawMaterialActualDate, cylinderCutterActualDate: row.cylinderCutterActualDate, laminationActualDate: row.laminationActualDate, printingActualDate: row.printingActualDate, etchingActualDate: row.etchingActualDate, slittingPackingActualDate: row.slittingPackingActualDate, bondingActualDate: row.bondingActualDate, cl60Cl822ActualDate: row.cl60Cl822ActualDate, spottingActualDate: row.spottingActualDate, encodingActualDate: row.encodingActualDate, dieCutActualDate: row.dieCutActualDate, inspectionActualDate: row.inspectionActualDate, deliveryPackageActualDate: row.deliveryPackageActualDate, comments: row.comments == null ? '' : String(row.comments), updateBy: this.$store.state.user.name })) batchUpdateProofTrackingPlan({ updateBy: this.$store.state.user.name, rows: payloadRows }).then(({ data }) => { if (data && data.code === 0) { this.$message.success('批量修改成功') this.batchEditMode = false this.getDataList() } else { this.$message.error(data.msg || '批量修改失败') } }).catch(() => { this.$message.error('批量修改异常') }) }, handleProcessDateChange (row, processCol, dateVal) { if (!row || !processCol) { return } const actualDate = this.formatDate(dateVal) // 业务要求:表格选择日期仅更新日期,不自动改变工序状态(状态由“√”动作控制)。
const 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 } // 若用户已手工选择日期,打勾仅改状态,不覆盖已选日期;无日期时才补当天。
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 } // 按页面交互约定:已完成工序允许双击日期输入框进行回退。
this.rollbackProcessComplete(row, processCol) }, rollbackProcessComplete (row, processCol) { if (!row || !processCol || !this.isProcessStatusComplete(row, processCol)) { return } const currentActualDate = this.formatDate(row[processCol.actualField]) this.$confirm('确定回退该工序状态为“未完成”吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 仅回退状态,保留当前实际完成日期,避免误回退时丢失用户已维护的日期信息。
this.updateProcessFromTable(row, processCol, { status: '未完成', actualDate: currentActualDate, remark: '表格一键回退为未完成' }, false, '工序状态已回退为未完成') }).catch(() => {}) }, 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() }) }, openProcessDialog (row, processCol) { if (!row || !processCol) { return } this.currentRow = row this.processForm = { trackingId: row.trackingId, processCode: processCol.code, processName: processCol.label, status: row[processCol.statusField] || '未完成', actualDate: row[processCol.actualField] || '', remark: '' } this.processHistoryRows = [] this.processDialogVisible = true this.loadProcessHistory(row, processCol, true) }, submitProcessUpdate () { if (!this.processForm.trackingId || !this.processForm.processCode) { this.$message.error('参数缺失') return } if (!this.processForm.status) { this.$message.error('请选择状态') return } const inData = Object.assign({}, this.processForm, { updateBy: this.$store.state.user.name }) this.saveProcessLoading = true updateProofTrackingProcess(inData).then(({ data }) => { this.saveProcessLoading = false if (data && data.code === 0) { this.$message.success('工序更新成功') this.processDialogVisible = false this.processHistoryMap = {} this.getDataList() } else { this.$message.error(data.msg || '工序更新失败') } }).catch(() => { this.saveProcessLoading = false this.$message.error('工序更新异常') }) }, finishProof (row) { const current = row || this.currentRow if (!current || !current.trackingId) { this.$message.warning('请先选择记录') return } this.finishForm = { trackingId: current.trackingId, actualityDeliveryDate: current.actualityDeliveryDate || this.formatDate(new Date()) } this.finishDialogVisible = true }, deleteProof (row) { const current = row || this.currentRow if (!current || !current.trackingId) { this.$message.warning('请先选择记录') return } this.$confirm('确定删除该打样记录吗?该操作仅删除打样记录,不会删除项目和项目物料。', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { deleteProofTracking({ trackingId: current.trackingId, updateBy: this.$store.state.user.name }).then(({ data }) => { if (data && data.code === 0) { this.$message.success('打样记录删除成功') this.processHistoryMap = {} this.resetProcessTooltip() this.getDataList() } else { this.$message.error((data && data.msg) || '删除打样记录失败') } }).catch(() => { this.$message.error('删除打样记录异常') }) }).catch(() => {}) }, submitFinishProof () { if (!this.finishForm.trackingId) { this.$message.warning('跟踪记录ID不能为空') return } if (!this.finishForm.actualityDeliveryDate) { this.$message.warning('请选择实际完成日期') return } this.finishSaveLoading = true finishProofTracking({ trackingId: this.finishForm.trackingId, actualityDeliveryDate: this.finishForm.actualityDeliveryDate, updateBy: this.$store.state.user.name }).then(({ data }) => { this.finishSaveLoading = false if (data && data.code === 0) { this.$message.success('打样完成成功') this.finishDialogVisible = false this.getDataList() } else { this.$message.error(data.msg || '打样完成失败') } }).catch(() => { this.finishSaveLoading = false this.$message.error('打样完成异常') }) }, getProcessCacheKey (row, processCol) { if (!row || !processCol || !row.trackingId) { return '' } return `${row.trackingId}_${processCol.code}` }, resetProcessTooltip () { this.processTooltipHoverKey = '' this.processTooltip.visible = false this.processTooltip.content = '' }, hideProcessTooltip () { this.processTooltip.visible = false this.processTooltip.content = '' }, updateProcessTooltipPosition (event) { if (!event) { return } const offsetX = 12 const offsetY = 16 let left = event.clientX + offsetX let top = event.clientY + offsetY const maxWidth = 220 const maxHeight = 120 if (left + maxWidth > window.innerWidth - 8) { left = window.innerWidth - maxWidth - 8 } if (top + maxHeight > window.innerHeight - 8) { top = event.clientY - maxHeight - 8 } this.processTooltip.left = Math.max(8, left) this.processTooltip.top = Math.max(8, top) }, handleProcessTooltipMouseMove (event) { if (!this.processTooltip.visible) { return } this.updateProcessTooltipPosition(event) }, handleProcessTooltipMouseLeave () { this.processTooltipHoverKey = '' this.hideProcessTooltip() }, handleProcessTooltipMouseEnter (row, processCol, event) { const cacheKey = this.getProcessCacheKey(row, processCol) if (!cacheKey) { return } this.processTooltipHoverKey = cacheKey this.updateProcessTooltipPosition(event) const showTooltip = () => { if (this.processTooltipHoverKey !== cacheKey) { return } if (!this.hasProcessHistory(row, processCol)) { this.hideProcessTooltip() return } const content = this.getProcessTooltip(row, processCol) if (!content) { this.hideProcessTooltip() return } this.processTooltip.content = content this.processTooltip.visible = true } if (Object.prototype.hasOwnProperty.call(this.processHistoryMap, cacheKey)) { showTooltip() return } this.loadProcessHistory(row, processCol, false).then(() => { showTooltip() }) }, loadProcessHistory (row, processCol, refreshDialog) { if (!row || !row.trackingId) { return Promise.resolve([]) } const cacheKey = this.getProcessCacheKey(row, processCol) if (Object.prototype.hasOwnProperty.call(this.processHistoryMap, cacheKey)) { const cachedRows = this.processHistoryMap[cacheKey] || [] if (refreshDialog) { this.processHistoryRows = cachedRows } return Promise.resolve(cachedRows) } return queryProofTrackingProcessHistory({ trackingId: row.trackingId, processCode: processCol.code }).then(({ data }) => { let rows = [] if (data && data.code === 0) { rows = data.rows || [] // Vue2 对对象新增 key 需要使用 $set 才能触发视图更新(tooltip 才会刷新历史列表)
this.$set(this.processHistoryMap, cacheKey, rows) if (refreshDialog) { this.processHistoryRows = rows } } return rows }).catch(() => { if (refreshDialog) { this.processHistoryRows = [] } this.$set(this.processHistoryMap, cacheKey, []) return [] }) }, hasProcessHistory (row, processCol) { if (!row || !processCol || !row.trackingId) { return false } const cacheKey = `${row.trackingId}_${processCol.code}` const rows = this.processHistoryMap[cacheKey] || [] return rows.length > 0 }, getProcessTooltip (row, processCol) { if (!row || !processCol) { return '' } const cacheKey = row.trackingId ? `${row.trackingId}_${processCol.code}` : '' const rows = this.processHistoryMap[cacheKey] || []
const dateList = [] const pushDate = (dateVal) => { const formatted = this.formatDate(dateVal) if (!formatted) { return } if (dateList.indexOf(formatted) === -1) { dateList.push(formatted) } }
// 先放当前实际完成日期,再放历史变更日期(实际)
pushDate(row[processCol.actualField]) rows.forEach(item => { pushDate(item.newActualDate) pushDate(item.oldActualDate) })
if (dateList.length <= 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) }, getTrackingStatusType (row) { if (!row) { return '' } const requiredDeliveryDate = this.formatDate(row.requiredDeliveryDate) if (!requiredDeliveryDate) { return '' } const today = this.formatDate(new Date()) // 按业务口径:今日 <= 预计完成日期 判定为 On schedule,其余判定为 Delay。
if (today <= requiredDeliveryDate) { return 'onSchedule' } return 'delay' }, getTrackingStatusText (row) { const statusType = this.getTrackingStatusType(row) if (!statusType) { return '-' } const statusPrefix = statusType === 'onSchedule' ? 'On schedule' : 'Delay' return `${statusPrefix}-${this.getFirstPendingProcessLabel(row)}` }, getTrackingStatusClass (row) { const statusType = this.getTrackingStatusType(row) if (statusType === 'onSchedule') { return 'tracking-status tracking-status--on-schedule' } if (statusType === 'delay') { return 'tracking-status tracking-status--delay' } return 'tracking-status tracking-status--unknown' }, formatDate (val) { if (!val) { return '' } if (typeof val === 'string') { return val.length > 10 ? val.substring(0, 10) : val } const date = new Date(val) if (isNaN(date.getTime())) { return '' } const y = date.getFullYear() const m = `${date.getMonth() + 1}`.padStart(2, '0') const d = `${date.getDate()}`.padStart(2, '0') return `${y}-${m}-${d}` }, formatDateTime (val) { if (!val) { return '' } if (typeof val === 'string') { return val.length > 19 ? val.substring(0, 19) : val } const date = new Date(val) if (isNaN(date.getTime())) { return '' } const y = date.getFullYear() const m = `${date.getMonth() + 1}`.padStart(2, '0') const d = `${date.getDate()}`.padStart(2, '0') const hh = `${date.getHours()}`.padStart(2, '0') const mm = `${date.getMinutes()}`.padStart(2, '0') const ss = `${date.getSeconds()}`.padStart(2, '0') return `${y}-${m}-${d} ${hh}:${mm}:${ss}` } }}</script>
<style >.data-table .cell { line-height: 26px; font-size: 12px; height: 28px;}
.search-form { margin-bottom: 0;}
.button-group-col { width: 100%; margin-top: 16px;}
.button-row { width: 100%; flex-wrap: wrap;}
.bu-process-config-dialog .bu-process-order-list { display: grid; grid-template-columns: repeat(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;}
.sync-text--ok { color: #67c23a; font-weight: 600;}
.sync-text--pending { color: #606266;}
.tracking-status { width: 100%; min-height: 26px; padding: 4px 6px; box-sizing: border-box; border: 1px solid #dcdfe6; border-radius: 4px; background: #fff; color: #303133; font-weight: 500; line-height: 18px; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
.tracking-status--on-schedule { background: #e8f5e9;}
.tracking-status--delay { background: #fde2e2;}
.tracking-status--unknown { background: #f4f4f5;}
.process-cell { background: #fff; min-height: 44px; padding: 4px 2px;}
.process-cell.is-complete { background: #ececec;}
.process-cell.is-complete .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-complete-btn { padding: 4px 8px; min-width: 12px;}
.comments-cell { display: flex; align-items: center; gap: 4px; min-height: 26px;}
.comments-edit-input { flex: 1;}
.comments-save-btn { padding: 4px 7px; min-width: 28px;}
.comments-text { width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
.process-date-tooltip { white-space: pre-line;}
.process-history-tooltip { position: fixed; z-index: 3000; max-width: 220px; padding: 10px 12px; border-radius: 4px; background: rgba(48, 49, 51, 0.95); color: #fff; font-size: 12px; line-height: 1.5; white-space: pre-line; pointer-events: none;}
.link-date { color: #0c4dbb; text-decoration: none;}
.link-date:hover,.link-date:focus { text-decoration: none;}
.actual-date { margin-top: 2px; color: #666;}
.finish-dialog-tip { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; padding: 8px 10px; border-radius: 4px; background: #f4f8ff; color: #3a4b5f; font-size: 12px;}
.finish-dialog-tip i { color: #409eff;}
.finish-tip-highlight { font-size: 13px; font-weight: 700; color: #1f2d3d;}
.finish-proof-dialog .el-dialog__body { padding-bottom: 8px;}
.finish-form .el-form-item { margin-bottom: 14px;}
.action-link { color: #0c4dbb; margin: 0 4px; cursor: pointer;}
.edit-link { color: #e6a23c;}
.delete-link { color: #f56c6c;}
.end-link { color: #a303ff;}
.zxClass .cell { line-height: 24px; font-size: 12px; height: 24px;}</style>
|