From e70b0394c2e5a6ac336905b93f4f36cf039e842e Mon Sep 17 00:00:00 2001 From: "han\\hanst" Date: Mon, 19 Oct 2026 09:55:47 +0800 Subject: [PATCH] 20261019 --- src/api/sampleTracking/sampleTracking.js | 2 + src/views/modules/common/column.vue | 188 +++++++++++++++--- src/views/modules/eam/eamProjectPartInfo.vue | 20 +- .../sampleTracking/projectProofTracking.vue | 128 ++++++------ 4 files changed, 254 insertions(+), 84 deletions(-) diff --git a/src/api/sampleTracking/sampleTracking.js b/src/api/sampleTracking/sampleTracking.js index bcda4e6..edefdb9 100644 --- a/src/api/sampleTracking/sampleTracking.js +++ b/src/api/sampleTracking/sampleTracking.js @@ -16,6 +16,8 @@ export const batchSyncProofTracking = data => createAPI('/sampleTracking/proofTr export const searchProofTracking = data => createAPI('/sampleTracking/proofTracking/search', 'POST', data) export const queryProofTrackingCustomerOptions = data => createAPI('/sampleTracking/proofTracking/customerOptions', 'POST', data) export const exportProofTracking = data => createAPI('/sampleTracking/proofTracking/export', 'POST', data, 'download') +export const exportProofTrackingPdf = data => createAPI('/sampleTracking/proofTracking/exportPdf', 'POST', data, 'download') +export const exportProofTrackingImage = data => createAPI('/sampleTracking/proofTracking/exportImage', 'POST', data, 'download') export const oneKeyCreateProofTracking = data => createAPI('/sampleTracking/proofTracking/oneKeyCreate', 'POST', data) export const oneKeyUpdateProofTracking = data => createAPI('/sampleTracking/proofTracking/oneKeyUpdate', 'POST', data) export const createProofTrackingRecord = data => createAPI('/sampleTracking/proofTracking/createProofRecord', 'POST', data) diff --git a/src/views/modules/common/column.vue b/src/views/modules/common/column.vue index 9b02b56..a707ee2 100644 --- a/src/views/modules/common/column.vue +++ b/src/views/modules/common/column.vue @@ -10,8 +10,10 @@ + width="88" + label="显示顺序" + align="center"> +import Sortable from 'sortablejs' import { saveTableUser, getTableDefaultListLanguage, @@ -232,9 +239,20 @@ export default { tableId: '', }, visible: false, - fullscreenLoading: false + fullscreenLoading: false, + columnSortable: null } }, + watch: { + visible (visible) { + if (!visible) { + this.destroyColumnSortable() + } + } + }, + beforeDestroy () { + this.destroyColumnSortable() + }, methods: { // 获取 用户的配置 @@ -254,29 +272,111 @@ export default { }, async getTableUserList(){ this.dataListLoading = true - await getTableUserListLanguage(this.queryTable).then(({data}) => { - if (data.code == 0) { - this.dataListLoading = false - this.userColumnList = data.rows - if (data.rows.length <= 0) { - getTableDefaultListLanguage(this.queryTable).then(({data}) => { - this.userColumnList = data.rows - }) - } - } - }) + this.destroyColumnSortable() + try { + const {data} = await getTableUserListLanguage(this.queryTable) + if (!data || data.code != 0) { + this.userColumnList = [] + return + } + let rows = Array.isArray(data.rows) ? data.rows : [] + if (rows.length <= 0) { + const defaultResult = await getTableDefaultListLanguage(this.queryTable) + rows = defaultResult && defaultResult.data && Array.isArray(defaultResult.data.rows) + ? defaultResult.data.rows + : [] + } + // 弹窗按显示顺序展示,拖动后的顺序才和列表栏位一致;不能沿用接口里“显示优先”的分组。 + this.userColumnList = this.sortColumnsBySortLv(rows) + } finally { + this.dataListLoading = false + this.initColumnSortable() + } }, - // 修改 table 列 + sortColumnsBySortLv (rows) { + return (rows || []).slice().sort((leftRow, rightRow) => { + const leftSort = parseInt(leftRow && leftRow.sortLv, 10) + const rightSort = parseInt(rightRow && rightRow.sortLv, 10) + const leftValue = Number.isNaN(leftSort) ? 0 : leftSort + const rightValue = Number.isNaN(rightSort) ? 0 : rightSort + return leftValue - rightValue + }) + }, + resetColumnSortLv () { + this.userColumnList.forEach((row, index) => { + if (!row) { + return + } + this.$set(row, 'sortLv', (index + 1) * 10) + }) + }, + destroyColumnSortable () { + if (this.columnSortable && typeof this.columnSortable.destroy === 'function') { + this.columnSortable.destroy() + } + this.columnSortable = null + }, + initColumnSortable () { + this.$nextTick(() => { + this.destroyColumnSortable() + if (!this.visible || !Array.isArray(this.userColumnList) || this.userColumnList.length === 0) { + return + } + const tableRef = this.$refs.columnSettingTable + const tableBody = tableRef && tableRef.$el + ? tableRef.$el.querySelector('.el-table__body-wrapper tbody') + : null + if (!tableBody) { + return + } + this.columnSortable = Sortable.create(tableBody, { + animation: 150, + handle: 'tr', + draggable: 'tr', + // 开关、下拉和宽度输入仍可操作,避免一按就拖行。 + filter: '.el-input, .el-select, .el-switch', + preventOnFilter: true, + ghostClass: 'column-sortable-ghost', + 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 nextRows = this.userColumnList.slice() + const movedRow = nextRows.splice(oldIndex, 1)[0] + nextRows.splice(newIndex, 0, movedRow) + nextRows.forEach((row, index) => { + if (row) { + row.sortLv = (index + 1) * 10 + } + }) + // 先销毁拖拽实例再重建表格,避免 Sortable 改过的 DOM 和 Vue 行序叠在一起。 + this.destroyColumnSortable() + this.userColumnList = [] + this.$nextTick(() => { + this.userColumnList = nextRows + this.initColumnSortable() + }) + } + }) + }) + }, + // 修改 table 列。拖动只改本地顺序,点确定后按当前行序一次性落库。 updateColumnList() { + if (!Array.isArray(this.userColumnList) || this.userColumnList.length === 0) { + this.$message.warning('没有可保存的栏位') + return + } + this.resetColumnSortLv() const loading = this.$loading({ lock: true, text: '正在保存用户设置', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }); - let userColumns = [] - for (let column of this.userColumnList) { - let userColumn = { + const userColumns = this.userColumnList.map(column => { + return { userId: this.$store.state.user.name, functionId: column.functionId, tableId: column.tableId, @@ -296,18 +396,19 @@ export default { serialNumber: column.serialNumber, columnType: column.columnType } - userColumns.push(userColumn) - this.$emit("refreshData"); - } + }) saveTableUser(userColumns).then(({data}) => { - if (data.code == 0) { + loading.close() + if (data && data.code == 0) { this.$message.success(data.msg) - loading.close(); this.visible = false this.$emit("refreshData", this.queryTable.tableId); } else { - this.$message.error(data.msg) + this.$message.error((data && data.msg) || '保存用户设置失败') } + }).catch(() => { + loading.close() + this.$message.error('保存用户设置失败') }) }, } @@ -326,4 +427,39 @@ export default { display: inline-block; } +.column-sort-tip { + margin: 6px 0; + color: #909399; + font-size: 12px; + line-height: 18px; +} + +.cl .el-table__body-wrapper tbody tr { + cursor: move; +} + +.column-drag-label { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: middle; +} + +.column-drag-handle { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + cursor: move; + user-select: none; + color: #606266; +} + +.column-sortable-ghost { + opacity: 0.65; + background: #e8f6f6; +} + diff --git a/src/views/modules/eam/eamProjectPartInfo.vue b/src/views/modules/eam/eamProjectPartInfo.vue index 3a563d1..73550c0 100644 --- a/src/views/modules/eam/eamProjectPartInfo.vue +++ b/src/views/modules/eam/eamProjectPartInfo.vue @@ -1250,6 +1250,7 @@ @@ -1262,6 +1263,7 @@ @@ -4863,10 +4865,11 @@ import {uploadFileList} from '@/api/base/baseFunction.js'; sqemeEngineer: row.sqemeEngineer || '', sqeeeEngineer: row.sqeeeEngineer || '', remark: '', - planStartDate : new Date(), + // 默认当天,按日期字符串绑定。直接放 Date 会在提交时被转成 UTC,东八区零点会少一天。 + planStartDate: moment().format('YYYY-MM-DD'), proofingNo: '', proofingStatus: '草稿', - rrequiredDeliveryDate: '', + requiredDeliveryDate: '', createDate: '', createBy: this.$store.state.user.name, } @@ -4924,6 +4927,9 @@ import {uploadFileList} from '@/api/base/baseFunction.js'; }) return } + // 后端按 yyyy-MM-dd 解析。若提交 Date,JSON 会先转 UTC,东八区选中的日期会落成前一天。 + this.newProofingRecordData.planStartDate = this.formatProofingDate(this.newProofingRecordData.planStartDate) + this.newProofingRecordData.requiredDeliveryDate = this.formatProofingDate(this.newProofingRecordData.requiredDeliveryDate) proofingInformationSave(this.newProofingRecordData).then(({data}) => { //区分请求成功和失败的状况 if (data && data.code === 0) { @@ -6773,6 +6779,16 @@ import {uploadFileList} from '@/api/base/baseFunction.js'; this.projectProductionValidationDocumentVisible = false; this.activeName1 = 'selectDocument'; }, + formatProofingDate (value) { + if (value === '' || value === null || value === undefined) { + return '' + } + const date = moment(value) + if (!date.isValid()) { + return '' + } + return date.format('YYYY-MM-DD') + }, formatDate(row, column) { // row 是当前行的数据对象 // column 是当前列的属性信息对象 diff --git a/src/views/modules/sampleTracking/projectProofTracking.vue b/src/views/modules/sampleTracking/projectProofTracking.vue index 7a823ae..95c45c0 100644 --- a/src/views/modules/sampleTracking/projectProofTracking.vue +++ b/src/views/modules/sampleTracking/projectProofTracking.vue @@ -30,7 +30,20 @@
查询 重置 - 导出 + + + 导出 + + + 导出Excel + 导出PDF + 导出图片 + + 一键创建 新增打样 同步项目物料到NPI @@ -919,7 +932,6 @@ /> -
仅可更改为终止、取消、报废,保存后同步到NPI
@@ -1125,7 +1137,7 @@
- + 0 @@ -4418,13 +4437,19 @@ export default { this.$message.error('查询异常') }) }, - async exportTrackingData () { + async downloadTrackingExport (exportType) { if (!this.validateSampleMakingDateRange()) { return } - if (this.exportLoading) { + if (this.exportLoading || this.exportPdfLoading || this.exportImageLoading) { return } + const exportTaskMap = { + excel: { loadingKey: 'exportLoading', failMessage: '导出失败', extension: 'xlsx', request: exportProofTracking }, + pdf: { loadingKey: 'exportPdfLoading', failMessage: '导出PDF失败', extension: 'pdf', request: exportProofTrackingPdf }, + image: { loadingKey: 'exportImageLoading', failMessage: '导出图片失败', extension: 'png', request: exportProofTrackingImage } + } + const exportTask = exportTaskMap[exportType] || exportTaskMap.excel this.searchData.site = this.$store.state.user.site this.searchData.userName = this.$store.state.user.name const exportTrackingColumns = this.visibleTrackingColumns.map(item => ({ @@ -4454,17 +4479,19 @@ export default { }) inData.proofingStatusList = normalizedProofingStatusList this.applyProofTrackingSearchFilters(inData) - this.exportLoading = true + const loadingKey = exportTask.loadingKey + const failMessage = exportTask.failMessage + this[loadingKey] = true try { - const response = await exportProofTracking(inData) + const response = await exportTask.request(inData) const blob = response && response.data ? response.data : null if (!blob) { - this.$message.error('导出失败') + this.$message.error(failMessage) return } if (blob.type && blob.type.indexOf('application/json') > -1) { const errorText = await blob.text() - let errorMsg = '导出失败' + let errorMsg = failMessage try { const errorObj = JSON.parse(errorText) if (errorObj && errorObj.msg) { @@ -4475,7 +4502,7 @@ export default { this.$message.error(errorMsg) return } - const fileName = this.resolveProofTrackingExportFileName(response) + const fileName = this.resolveProofTrackingExportFileName(response, exportTask.extension) const url = window.URL.createObjectURL(blob) const link = document.createElement('a') link.href = url @@ -4485,13 +4512,13 @@ export default { link.remove() window.URL.revokeObjectURL(url) } catch (e) { - this.$message.error('导出失败') + this.$message.error(failMessage) } finally { - this.exportLoading = false + this[loadingKey] = false } }, - resolveProofTrackingExportFileName (response) { - const defaultFileName = `项目打样跟踪_${this.getExportTimestamp()}.xlsx` + resolveProofTrackingExportFileName (response, extension) { + const defaultFileName = `项目打样跟踪_${this.getExportTimestamp()}.${extension || 'xlsx'}` const disposition = response && response.headers ? (response.headers['content-disposition'] || response.headers['Content-Disposition'] || '') : '' @@ -4923,7 +4950,7 @@ export default { return } optionMap[applyNo] = true - // 预计完成日期、创建日期分别回填打样的预计完成/开始日期,避免二次查单。 + // 试验单只提供创建日期,用来补打样开始日期。预计完成日期和 baseline 不从试验单带入。 optionRows.push({ applyNo: applyNo, expectedFinishDate: this.formatDate(item.expectedFinishDate), @@ -4944,6 +4971,7 @@ export default { this.oneKeyProofingApplyOptions = [] this.oneKeyForm.proofingNo = '' this.oneKeyForm.requiredDeliveryDate = '' + this.oneKeyForm.baseline = '' this.oneKeyForm.planStartDate = '' this.oneKeyProjectNoSnapshot = '' this.oneKeyForm.testPartNo = '' @@ -4967,7 +4995,7 @@ export default { handleOneKeyProofingNoChange (proofingNo) { const selectedNo = this.isBlankValue(proofingNo) ? '' : String(proofingNo).trim() if (this.oneKeyDialogMode !== 'create') { - // 修改模式:库里已有日期保留;为空时才跟随打样单号回填,避免空日期一直缺值。 + // 修改和详情以库里的预计完成日期、baseline 为准。库里为空就保持空,不用试验单预计完成日期回填。 if (!selectedNo) { this.oneKeyForm.proofingNo = '' return @@ -4975,12 +5003,15 @@ export default { const matchedInEdit = this.oneKeyProofingApplyOptions.find(item => item && item.applyNo === selectedNo) if (matchedInEdit) { this.oneKeyForm.proofingNo = matchedInEdit.applyNo - this.fillEmptyOneKeyProofingDatesFromApply(matchedInEdit) + if (this.isBlankValue(this.oneKeyForm.planStartDate)) { + this.oneKeyForm.planStartDate = matchedInEdit.createTime || '' + } } return } if (!selectedNo) { this.oneKeyForm.requiredDeliveryDate = '' + this.oneKeyForm.baseline = '' this.oneKeyForm.planStartDate = '' return } @@ -4989,27 +5020,9 @@ export default { return } this.oneKeyForm.proofingNo = matched.applyNo - this.oneKeyForm.requiredDeliveryDate = matched.expectedFinishDate || '' - // 打样开始日期默认取工程实验单创建日期;清空打样单时同步清空,避免残留旧单日期。 + // 一键创建不带试验单预计完成日期。预计完成日期和 baseline 保持空白,保存时也就不会生成 Delivery&Package。 + // 打样开始日期仍取工程实验单创建日期;清空打样单时同步清空,避免残留旧单日期。 this.oneKeyForm.planStartDate = matched.createTime || '' - this.syncOneKeyBaselineByRequiredDeliveryDate(this.oneKeyForm.requiredDeliveryDate, false) - }, - fillEmptyOneKeyProofingDatesFromApply (matched) { - if (!matched) { - return - } - let filledRequiredDate = false - if (this.isBlankValue(this.oneKeyForm.requiredDeliveryDate)) { - this.oneKeyForm.requiredDeliveryDate = matched.expectedFinishDate || '' - filledRequiredDate = true - } - if (this.isBlankValue(this.oneKeyForm.planStartDate)) { - this.oneKeyForm.planStartDate = matched.createTime || '' - } - // 仅在本次补了预计完成日期时尝试带出 baseline,避免覆盖修改态已有 baseline。 - if (filledRequiredDate) { - this.syncOneKeyBaselineByRequiredDeliveryDate(this.oneKeyForm.requiredDeliveryDate, false) - } }, handleOneKeyRequiredDeliveryDateChange (dateVal) { this.syncOneKeyBaselineByRequiredDeliveryDate(dateVal, false) @@ -5059,6 +5072,7 @@ export default { if (!hasCurrent && !keepCurrentIfMissing) { this.oneKeyForm.proofingNo = '' this.oneKeyForm.requiredDeliveryDate = '' + this.oneKeyForm.baseline = '' this.oneKeyForm.planStartDate = '' } } @@ -5083,6 +5097,7 @@ export default { const selectedNo = this.isBlankValue(proofingNo) ? '' : String(proofingNo).trim() if (!selectedNo) { this.proofDialogData.requiredDeliveryDate = '' + this.proofDialogData.baseline = '' this.proofDialogData.planStartDate = '' return } @@ -5091,10 +5106,9 @@ export default { return } this.proofDialogData.proofingNo = matched.applyNo - this.proofDialogData.requiredDeliveryDate = matched.expectedFinishDate || '' - // 打样开始日期默认取工程实验单创建日期;清空打样单时同步清空,避免残留旧单日期。 + // 新增打样不带试验单预计完成日期。预计完成日期和 baseline 保持空白,保存时也就不会生成 Delivery&Package。 + // 打样开始日期仍取工程实验单创建日期;清空打样单时同步清空,避免残留旧单日期。 this.proofDialogData.planStartDate = matched.createTime || '' - this.syncProofDialogBaselineByRequiredDeliveryDate(this.proofDialogData.requiredDeliveryDate, false) }, handleProofDialogRequiredDeliveryDateChange (dateVal) { this.syncProofDialogBaselineByRequiredDeliveryDate(dateVal, false) @@ -5119,6 +5133,7 @@ export default { this.proofDialogApplyOptions = [] this.proofDialogData.proofingNo = '' this.proofDialogData.requiredDeliveryDate = '' + this.proofDialogData.baseline = '' this.proofDialogData.planStartDate = '' return Promise.resolve([]) } @@ -5574,9 +5589,13 @@ export default { inData.priorityLevel = '' } if (this.oneKeyDialogMode === 'create' && this.isBlankValue(inData.baseline) && !this.isBlankValue(inData.requiredDeliveryDate)) { - // 一键创建若已带出 Delivery&Package 日期,则 baseline 默认同步该日期(允许用户提交前改写)。 + // 仅当用户自己填写了预计完成日期时,baseline 才跟随;选择打样单号不会再带出试验单日期。 inData.baseline = this.formatDate(inData.requiredDeliveryDate) } + if (this.oneKeyDialogMode === 'create') { + inData.requiredDeliveryDate = this.isBlankValue(inData.requiredDeliveryDate) ? null : this.formatDate(inData.requiredDeliveryDate) + inData.baseline = this.isBlankValue(inData.baseline) ? null : this.formatDate(inData.baseline) + } // 干湿分类仅在 RFID 使用,非 RFID 提交时统一清空,避免旧值串到错误 BU。 if (!this.isRfidBuNo(inData.buNo)) { inData.dry_wet_type = '' @@ -6163,10 +6182,6 @@ export default { this.$message.warning('打样开始日期不能为空') return false } - if (!this.proofDialogData.requiredDeliveryDate) { - this.$message.warning('预计完成日期不能为空') - return false - } return true }, resolveProofDialogBaseline () { @@ -6192,7 +6207,7 @@ export default { proofingNo: this.proofDialogData.proofingNo, proofingNumber: Number(this.proofDialogData.proofingNumber), planStartDate: this.proofDialogData.planStartDate, - requiredDeliveryDate: this.proofDialogData.requiredDeliveryDate, + requiredDeliveryDate: this.isBlankValue(this.proofDialogData.requiredDeliveryDate) ? null : this.proofDialogData.requiredDeliveryDate, baseline: this.resolveProofDialogBaseline(), actualityDeliveryDate: this.proofDialogData.actualityDeliveryDate || null, pmInqueryTime: this.isBlankValue(this.proofDialogData.pmInqueryTime) ? null : this.proofDialogData.pmInqueryTime, @@ -6314,11 +6329,12 @@ export default { // 业务要求:表格选择日期仅更新日期,不自动改变工序状态(状态由“√”动作控制)。 const currentStatus = row[processCol.statusField] ? String(row[processCol.statusField]).trim() : '' const nextStatus = currentStatus || '未完成' + // 只改日期时当前行已回写,不再整表刷新,避免滚动位置和日期编辑态被列表重查清掉。 this.updateProcessFromTable(row, processCol, { status: nextStatus, actualDate: actualDate, remark: actualDate ? '表格直接修改实际完成日期(状态不变)' : '表格清空实际完成日期(状态不变)' - }, true) + }, true, '', true) }, markProcessComplete (row, processCol) { if (!row || !processCol || this.isProcessStatusComplete(row, processCol)) { @@ -6410,7 +6426,7 @@ export default { } return '-' }, - updateProcessFromTable (row, processCol, processData, silentSuccess, successMsg) { + updateProcessFromTable (row, processCol, processData, silentSuccess, successMsg, skipListRefresh) { if (!row || !processCol || !row.trackingId) { this.$message.error('工序参数缺失') return @@ -6440,8 +6456,10 @@ export default { if (!silentSuccess) { this.$message.success(successMsg || '工序更新成功') } - // 按业务要求:工序日期或状态变更后回查列表,保证动态列/派生字段展示最新值。 - this.getDataList() + // 改日期只更新当前单元格。打勾、回退仍回查列表,因为状态变化会影响打样状态和派生列。 + if (!skipListRefresh) { + this.getDataList() + } } else { this.$message.error(data.msg || '工序更新失败') this.getDataList() @@ -7214,6 +7232,10 @@ export default { row-gap: 6px; } +.mod-config .search-header-actions .tracking-export-dropdown { + margin: 0 10px; +} + .search-proofing-status-select .el-select__tags { flex-wrap: nowrap; overflow-x: auto; @@ -7452,12 +7474,6 @@ export default { cursor: pointer; } -.proofing-status-change-tip { - margin-top: 2px; - color: #909399; - font-size: 12px; - line-height: 16px; -} .npi-jump-link:hover { text-decoration: underline;