diff --git a/src/api/sampleTracking/sampleTracking.js b/src/api/sampleTracking/sampleTracking.js index aca08ed..c2ec7e1 100644 --- a/src/api/sampleTracking/sampleTracking.js +++ b/src/api/sampleTracking/sampleTracking.js @@ -25,3 +25,4 @@ export const deleteProofTracking = data => createAPI('/sampleTracking/proofTrack export const queryProofTrackingProcessHistory = data => createAPI('/sampleTracking/proofTracking/processHistory', 'POST', data) export const queryProofTrackingBuProcessConfig = data => createAPI('/sampleTracking/proofTracking/buProcessConfig/list', 'POST', data) export const saveProofTrackingBuProcessConfig = data => createAPI('/sampleTracking/proofTracking/buProcessConfig/save', 'POST', data) +export const queryProofTrackingProcessColumns = data => createAPI('/sampleTracking/proofTracking/processColumns/list', 'POST', data) diff --git a/src/views/modules/sampleTracking/projectProofTracking.vue b/src/views/modules/sampleTracking/projectProofTracking.vue index 78a28ec..3451066 100644 --- a/src/views/modules/sampleTracking/projectProofTracking.vue +++ b/src/views/modules/sampleTracking/projectProofTracking.vue @@ -110,8 +110,8 @@ +
+ + 拖动可调整顺序,勾选控制可见列 +
@@ -524,7 +538,7 @@ - + Object.assign({}, item)) +} + export default { name: 'ProjectProofTracking', directives: { @@ -894,24 +929,14 @@ export default { proofSyncFlag: '', proofingStatus: '' }, - processColumns: [ - { label: 'Raw Material', code: 'rawMaterial', planField: 'rawMaterialPlanDate', actualField: 'rawMaterialActualDate', statusField: 'rawMaterialStatus' }, - { label: 'Cylinder/Cutter', code: 'cylinderCutter', planField: 'cylinderCutterPlanDate', actualField: 'cylinderCutterActualDate', statusField: 'cylinderCutterStatus' }, - { label: 'Lamination', code: 'lamination', planField: 'laminationPlanDate', actualField: 'laminationActualDate', statusField: 'laminationStatus' }, - { label: 'Printing', code: 'printing', planField: 'printingPlanDate', actualField: 'printingActualDate', statusField: 'printingStatus' }, - { label: 'Etching', code: 'etching', planField: 'etchingPlanDate', actualField: 'etchingActualDate', statusField: 'etchingStatus' }, - { label: 'Slitting&Packing', code: 'slittingPacking', planField: 'slittingPackingPlanDate', actualField: 'slittingPackingActualDate', statusField: 'slittingPackingStatus' }, - { label: 'Bonding', code: 'bonding', planField: 'bondingPlanDate', actualField: 'bondingActualDate', statusField: 'bondingStatus' }, - { label: 'CL60/CL822', code: 'cl60Cl822', planField: 'cl60Cl822PlanDate', actualField: 'cl60Cl822ActualDate', statusField: 'cl60Cl822Status' }, - { label: 'Spotting', code: 'spotting', planField: 'spottingPlanDate', actualField: 'spottingActualDate', statusField: 'spottingStatus' }, - { label: 'Encoding', code: 'encoding', planField: 'encodingPlanDate', actualField: 'encodingActualDate', statusField: 'encodingStatus' }, - { label: 'Die cut', code: 'dieCut', planField: 'dieCutPlanDate', actualField: 'dieCutActualDate', statusField: 'dieCutStatus' }, - { label: 'Inspection', code: 'inspection', planField: 'inspectionPlanDate', actualField: 'inspectionActualDate', statusField: 'inspectionStatus' }, - { label: 'Delivery&Package', code: 'deliveryPackage', planField: 'deliveryPackagePlanDate', actualField: 'deliveryPackageActualDate', statusField: 'deliveryPackageStatus' } - ], + processColumns: cloneDefaultProcessColumns(), buProcessConfigDialogVisible: false, buProcessConfigRows: [], buProcessConfigMap: {}, + processOrderDragState: { + buNo: '', + fromIndex: -1 + }, projectCategoryList: ['Low Risk', 'High Risk', 'Sustaining'], cProjectRegionList: [ { cProjectRegion: 'Global' }, @@ -1056,7 +1081,7 @@ export default { } }, activated () { - this.loadUserBuList().then(() => { + Promise.all([this.loadUserBuList(), this.queryProcessColumns()]).then(() => { this.queryBuProcessConfig() }) this.getDataList() @@ -1099,9 +1124,168 @@ export default { return [] }) }, + queryProcessColumns () { + const selectedBuNo = this.getSelectedSearchBuNo() + const queryBuNo = selectedBuNo || '*' + // 列顺序来源规则: + // 1) 未选择查询 BU:固定按 bu='*' 读取默认工序顺序; + // 2) 已选择查询 BU:按该 BU 读取;若无配置由后端回退到 bu='*'。 + const inData = { + site: this.$store.state.user.site, + userName: this.$store.state.user.name, + buNo: queryBuNo + } + return queryProofTrackingProcessColumns(inData).then(({ data }) => { + if (!(data && data.code === 0)) { + this.$message.error((data && data.msg) || '加载工序列配置失败,已使用默认配置') + this.processColumns = cloneDefaultProcessColumns() + this.ensureBuProcessConfigRows() + this.refreshTrackingTableLayout() + return this.processColumns + } + const rows = Array.isArray(data.rows) ? data.rows : [] + // 防御性排序:即使后端 SQL 已 ORDER BY sort_no,前端仍按 sortNo 二次排序,确保展示稳定。 + const sortedRows = rows + .map((item, index) => { + const parsedSortNo = parseInt(item && item.sortNo, 10) + return { + item: item, + sourceIndex: index, + sortNo: Number.isNaN(parsedSortNo) ? Number.MAX_SAFE_INTEGER : parsedSortNo + } + }) + .sort((a, b) => { + if (a.sortNo !== b.sortNo) { + return a.sortNo - b.sortNo + } + return a.sourceIndex - b.sourceIndex + }) + const nextColumns = [] + const usedCodeMap = {} + sortedRows.forEach(({ item, sortNo }) => { + if (!item) { + return + } + const code = item.code == null ? '' : String(item.code).trim() + const label = item.label == null ? '' : String(item.label).trim() + const planField = item.planField == null ? '' : String(item.planField).trim() + const actualField = item.actualField == null ? '' : String(item.actualField).trim() + const statusField = item.statusField == null ? '' : String(item.statusField).trim() + if (!code || !label || !planField || !actualField || !statusField || usedCodeMap[code]) { + return + } + usedCodeMap[code] = true + nextColumns.push({ + label: label, + code: code, + planField: planField, + actualField: actualField, + statusField: statusField, + sortNo: sortNo === Number.MAX_SAFE_INTEGER ? null : sortNo + }) + }) + // 保障切换期可用性:当表里无有效配置时继续使用前端兜底工序定义。 + this.processColumns = nextColumns.length > 0 ? nextColumns : cloneDefaultProcessColumns() + this.syncDataListProcessValues() + this.ensureBuProcessConfigRows() + this.refreshTrackingTableLayout() + return this.processColumns + }).catch(() => { + this.$message.error('加载工序列配置异常,已使用默认配置') + this.processColumns = cloneDefaultProcessColumns() + this.syncDataListProcessValues() + this.ensureBuProcessConfigRows() + this.refreshTrackingTableLayout() + return this.processColumns + }) + }, + getRowProcessValue (row, processCode) { + if (!row || !processCode) { + return null + } + const processMap = row.processValueMap && typeof row.processValueMap === 'object' + ? row.processValueMap + : null + if (!processMap) { + return null + } + return processMap[processCode] || null + }, + applyProcessValuesToRow (row) { + if (!row || !this.processColumns || this.processColumns.length === 0) { + return + } + this.processColumns.forEach(item => { + if (!item) { + return + } + const processValue = this.getRowProcessValue(row, item.code) + const planDate = processValue ? this.formatDate(processValue.planDate) : '' + const actualDate = processValue ? this.formatDate(processValue.actualDate) : '' + const status = processValue && processValue.status ? String(processValue.status).trim() : '' + this.$set(row, item.planField, planDate) + this.$set(row, item.actualField, actualDate) + this.$set(row, item.statusField, status) + }) + }, + syncDataListProcessValues () { + if (!Array.isArray(this.dataList) || this.dataList.length === 0) { + return + } + this.dataList.forEach(row => { + this.applyProcessValuesToRow(row) + }) + }, + updateRowProcessValue (row, processCol, status, actualDate) { + if (!row || !processCol) { + return + } + if (!row.processValueMap || typeof row.processValueMap !== 'object') { + this.$set(row, 'processValueMap', {}) + } + const prevValue = this.getRowProcessValue(row, processCol.code) || {} + this.$set(row.processValueMap, processCol.code, Object.assign({}, prevValue, { + processCode: processCol.code, + status: status, + actualDate: actualDate || null + })) + this.$set(row, processCol.statusField, status) + this.$set(row, processCol.actualField, actualDate) + }, getAllProcessCodes () { return this.processColumns.map(item => item.code) }, + getProcessLabelByCode (processCode) { + if (!processCode) { + return '' + } + const matched = this.processColumns.find(item => item && item.code === processCode) + if (matched && matched.label) { + return matched.label + } + const defaultMatched = DEFAULT_PROCESS_COLUMNS.find(item => item && item.code === processCode) + return defaultMatched && defaultMatched.label ? defaultMatched.label : processCode + }, + getValidProcessOrderCodes (codes) { + // 顺序列表不能因为“当前 BU 不可见”而丢项:先保留后端返回顺序,再补齐当前已知工序。 + const orderedCodes = [] + if (Array.isArray(codes)) { + codes.forEach(code => { + const codeVal = code == null ? '' : String(code).trim() + if (!codeVal || orderedCodes.indexOf(codeVal) > -1) { + return + } + orderedCodes.push(codeVal) + }) + } + const allCodes = this.getAllProcessCodes() + allCodes.forEach(code => { + if (orderedCodes.indexOf(code) === -1) { + orderedCodes.push(code) + } + }) + return orderedCodes + }, normalizeBuNo (buNo, site) { const rawBuNo = buNo == null ? '' : String(buNo).trim() if (!rawBuNo) { @@ -1114,11 +1298,15 @@ export default { // BU统一按“去掉site前缀”展示与保存,例如 2_01-Label -> 01-Label。 return rawBuNo.substring(splitIdx + 1) }, - getValidProcessCodes (codes) { + getValidProcessCodes (codes, allowedCodes) { if (!Array.isArray(codes) || codes.length === 0) { return [] } - const allCodes = this.getAllProcessCodes() + 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() @@ -1147,7 +1335,9 @@ export default { }, handleSearchBuChange () { // BU筛选变化(含清空)后立即重新查询,并回到第一页。 - this.getDataList('Y') + this.queryProcessColumns().then(() => { + this.getDataList('Y') + }) }, getProcessVisibleCodesByBuNo (buNo, site) { const configEntry = this.getBuProcessConfigEntry(buNo, site) @@ -1188,19 +1378,24 @@ export default { } const rows = data.rows || [] const nextMap = {} - const allCodeCount = this.processColumns.length rows.forEach(item => { const buNo = this.normalizeBuNo(item && item.buNo ? item.buNo : '', this.$store.state.user.site) if (!buNo) { return } - const processCodes = this.getValidProcessCodes(item.processCodes) - const showAll = item.showAll !== false || processCodes.length === 0 || processCodes.length >= allCodeCount - if (!showAll) { - nextMap[buNo] = { - showAll: false, - processCodes: processCodes - } + const processOrderCodes = this.getValidProcessOrderCodes(item.processOrderCodes) + const processCodes = this.getValidProcessCodes(item.processCodes, processOrderCodes) + const showAll = item && item.showAll === true + ? true + : processCodes.length >= processOrderCodes.length + const selectedCodeMap = {} + processCodes.forEach(code => { + selectedCodeMap[code] = true + }) + nextMap[buNo] = { + showAll: showAll, + processOrderCodes: processOrderCodes, + processCodes: showAll ? processOrderCodes.slice() : processOrderCodes.filter(code => selectedCodeMap[code]) } }) this.buProcessConfigMap = nextMap @@ -1224,12 +1419,23 @@ export default { const row = { buNo: key, buDesc: buDesc || key, + processOrderCodes: allCodes.slice(), processCodes: allCodes.slice() } - if (cfg && cfg.showAll === false) { - const validCodes = this.getValidProcessCodes(cfg.processCodes) - if (validCodes.length > 0 && validCodes.length < allCodes.length) { - row.processCodes = validCodes + if (cfg) { + const validOrderCodes = this.getValidProcessOrderCodes(cfg.processOrderCodes) + row.processOrderCodes = validOrderCodes + if (cfg.showAll === false) { + const validCodes = this.getValidProcessCodes(cfg.processCodes, validOrderCodes) + if (validCodes.length > 0 && validCodes.length < validOrderCodes.length) { + const selectedCodeMap = {} + validCodes.forEach(code => { + selectedCodeMap[code] = true + }) + row.processCodes = validOrderCodes.filter(code => selectedCodeMap[code]) + } + } else { + row.processCodes = validOrderCodes.slice() } } rows.push(row) @@ -1247,6 +1453,10 @@ export default { }, openBuProcessConfigDialog () { this.ensureBuProcessConfigRows() + this.processOrderDragState = { + buNo: '', + fromIndex: -1 + } this.buProcessConfigDialogVisible = true }, refreshTrackingTableLayout () { @@ -1261,7 +1471,68 @@ export default { if (!row) { return } - row.processCodes = this.getValidProcessCodes(row.processCodes) + const validOrderCodes = this.getValidProcessOrderCodes(row.processOrderCodes) + const validCodes = this.getValidProcessCodes(row.processCodes, validOrderCodes) + const selectedCodeMap = {} + validCodes.forEach(code => { + selectedCodeMap[code] = true + }) + row.processOrderCodes = validOrderCodes + row.processCodes = validOrderCodes.filter(code => selectedCodeMap[code]) + }, + isProcessOrderDraggingItem (row, index) { + if (!row || index == null) { + return false + } + return this.processOrderDragState.buNo === row.buNo && this.processOrderDragState.fromIndex === index + }, + handleProcessOrderDragStart (row, index, event) { + if (!row || index == null || index < 0) { + return + } + this.processOrderDragState = { + buNo: row.buNo || '', + fromIndex: index + } + if (event && event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move' + } + }, + handleProcessOrderDrop (row, targetIndex, event) { + if (event && event.dataTransfer) { + event.dataTransfer.dropEffect = 'move' + } + if (!row || targetIndex == null || targetIndex < 0) { + this.handleProcessOrderDragEnd() + return + } + const dragBuNo = this.processOrderDragState.buNo + const fromIndex = Number(this.processOrderDragState.fromIndex) + if (!dragBuNo || dragBuNo !== row.buNo || Number.isNaN(fromIndex) || fromIndex < 0) { + this.handleProcessOrderDragEnd() + return + } + const validOrderCodes = this.getValidProcessOrderCodes(row.processOrderCodes) + if (fromIndex >= validOrderCodes.length || targetIndex >= validOrderCodes.length) { + this.handleProcessOrderDragEnd() + return + } + if (fromIndex === targetIndex) { + this.handleProcessOrderDragEnd() + return + } + const movedCode = validOrderCodes.splice(fromIndex, 1)[0] + const insertIndex = targetIndex + validOrderCodes.splice(insertIndex, 0, movedCode) + row.processOrderCodes = validOrderCodes + this.handleBuProcessCodesChange(row) + this.handleProcessOrderDragEnd() + }, + handleProcessOrderDragEnd () { + this.processOrderDragState = { + buNo: '', + fromIndex: -1 + } }, saveBuProcessConfig () { if (!this.buProcessConfigRows || this.buProcessConfigRows.length === 0) { @@ -1269,7 +1540,6 @@ export default { return } const nextMap = {} - const allCodeCount = this.processColumns.length const submitRows = [] for (let i = 0; i < this.buProcessConfigRows.length; i++) { const row = this.buProcessConfigRows[i] @@ -1277,22 +1547,27 @@ export default { if (!buNo) { continue } - const validCodes = this.getValidProcessCodes(row.processCodes) + const validOrderCodes = this.getValidProcessOrderCodes(row.processOrderCodes) + const selectedCodeMap = {} + this.getValidProcessCodes(row.processCodes, validOrderCodes).forEach(code => { + selectedCodeMap[code] = true + }) + const validCodes = validOrderCodes.filter(code => selectedCodeMap[code]) if (validCodes.length === 0) { this.$message.warning(`事业部【${row.buDesc || buNo}】至少需要选择一个可见进度`) return } - const showAll = validCodes.length >= allCodeCount - if (!showAll) { - nextMap[buNo] = { - showAll: false, - processCodes: validCodes - } + const showAll = validCodes.length >= validOrderCodes.length + nextMap[buNo] = { + showAll: showAll, + processOrderCodes: validOrderCodes, + processCodes: showAll ? validOrderCodes.slice() : validCodes } submitRows.push({ buNo: buNo, showAll: showAll, - processCodes: validCodes + processCodes: validCodes, + processOrderCodes: validOrderCodes }) } const inData = { @@ -1309,7 +1584,11 @@ export default { this.ensureBuProcessConfigRows() this.buProcessConfigDialogVisible = false this.$message.success(data.msg || '事业部进度展示设置已保存') - this.refreshTrackingTableLayout() + // 保存后重新拉取工序列和列表,确保页面立即按最新可见性与顺序刷新。 + this.queryProcessColumns().then(() => { + this.getDataList() + this.refreshTrackingTableLayout() + }) }).catch(() => { this.$message.error('事业部进度展示设置保存异常') }) @@ -1614,6 +1893,7 @@ export default { 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) @@ -1667,7 +1947,9 @@ export default { this.searchData.projectPartSyncFlag = '' this.searchData.proofSyncFlag = '' this.searchData.proofingStatus = '' - this.getDataList('Y') + this.queryProcessColumns().then(() => { + this.getDataList('Y') + }) }, selectionChange (rows) { this.selectionRows = rows || [] @@ -2432,10 +2714,12 @@ export default { return } const actualDate = this.formatDate(dateVal) + // 工序日期被清空时,状态必须回退为“未完成”,避免出现“无日期但已完成”的脏状态。 + const nextStatus = actualDate ? '已完成' : '未完成' this.updateProcessFromTable(row, processCol, { - status: '已完成', + status: nextStatus, actualDate: actualDate, - remark: '表格直接选择日期' + remark: actualDate ? '表格直接选择日期' : '表格清空实际完成日期' }, true) }, markProcessComplete (row, processCol) { @@ -2471,8 +2755,7 @@ export default { } updateProofTrackingProcess(inData).then(({ data }) => { if (data && data.code === 0) { - row[processCol.statusField] = status - row[processCol.actualField] = actualDate + this.updateRowProcessValue(row, processCol, status, actualDate) const cacheKey = `${row.trackingId}_${processCol.code}` if (this.processHistoryMap[cacheKey]) { this.$delete(this.processHistoryMap, cacheKey) @@ -2828,7 +3111,7 @@ export default { flex-wrap: wrap; } -.bu-process-config-dialog .el-checkbox-group { +.bu-process-config-dialog .bu-process-order-list { display: grid; grid-template-columns: repeat(5, minmax(110px, 1fr)); column-gap: 12px; @@ -2849,16 +3132,60 @@ export default { vertical-align: top; } -.bu-process-config-dialog .bu-process-list-column .el-checkbox, -.bu-process-config-dialog .bu-process-list-column .el-checkbox + .el-checkbox { +.bu-process-config-dialog .bu-process-order-item { + display: flex; + align-items: center; + min-height: 28px; + padding: 2px 6px; + border: 1px solid transparent; + border-radius: 4px; + cursor: move; + transition: border-color .2s ease, background-color .2s ease; +} + +.bu-process-config-dialog .bu-process-order-item:hover { + border-color: #7cc4ff; + background: #f5fbff; +} + +.bu-process-config-dialog .bu-process-order-item.is-dragging { + opacity: .55; + border-color: #409eff; + background: #ecf5ff; +} + +.bu-process-config-dialog .process-order-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-list-column .el-checkbox__label { +.bu-process-config-dialog .bu-process-order-item .el-checkbox__label { white-space: nowrap; } +.bu-process-config-dialog .bu-process-config-tip { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 8px; + padding: 6px 10px; + border-radius: 4px; + background: #f4f8ff; + color: #5f6b7a; + font-size: 12px; +} + +.bu-process-config-dialog .bu-process-config-tip i { + color: #409eff; +} + .button-col { margin-bottom: 8px; }