From c2f58c61cfe9a2b0fbe0db0152cd3ff08a73e070 Mon Sep 17 00:00:00 2001 From: "han\\hanst" Date: Mon, 9 Nov 2026 11:13:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B1=87=E7=8E=87=EF=BC=9B=E6=89=93=E6=A0=B7?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=8F=AF=E4=BB=A5=E6=94=B9=E4=B8=BA=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/sampleTracking/sampleTracking.js | 3 + .../sampleTracking/projectProofTracking.vue | 708 +++++++++++++++++- 2 files changed, 703 insertions(+), 8 deletions(-) diff --git a/src/api/sampleTracking/sampleTracking.js b/src/api/sampleTracking/sampleTracking.js index edefdb9..76767cc 100644 --- a/src/api/sampleTracking/sampleTracking.js +++ b/src/api/sampleTracking/sampleTracking.js @@ -23,6 +23,9 @@ export const oneKeyUpdateProofTracking = data => createAPI('/sampleTracking/proo export const createProofTrackingRecord = data => createAPI('/sampleTracking/proofTracking/createProofRecord', 'POST', data) export const updateProofTrackingProcess = data => createAPI('/sampleTracking/proofTracking/updateProcess', 'POST', data) export const updateProofTrackingComments = data => createAPI('/sampleTracking/proofTracking/updateComments', 'POST', data) +export const updateProofTrackingCost = data => createAPI('/sampleTracking/proofTracking/updateCost', 'POST', data) +export const saveProofTrackingExchangeRate = data => createAPI('/sampleTracking/proofTracking/saveExchangeRate', 'POST', data) +export const refreshProofTrackingCostByRate = data => createAPI('/sampleTracking/proofTracking/refreshCostByRate', 'POST', data) export const finishProofTracking = data => createAPI('/sampleTracking/proofTracking/finishProofing', 'POST', data) export const deleteProofTracking = data => createAPI('/sampleTracking/proofTracking/delete', 'POST', data) export const deleteProofTrackingBatch = data => createAPI('/sampleTracking/proofTracking/deleteBatch', 'POST', data) diff --git a/src/views/modules/sampleTracking/projectProofTracking.vue b/src/views/modules/sampleTracking/projectProofTracking.vue index f106b09..2c3a308 100644 --- a/src/views/modules/sampleTracking/projectProofTracking.vue +++ b/src/views/modules/sampleTracking/projectProofTracking.vue @@ -58,6 +58,7 @@ >事业部进度设置 默认进度设置 删除 + 按汇率重算 @@ -67,6 +68,19 @@ + + 汇率 + + @@ -298,6 +312,49 @@ v-else-if="item.columnProp === 'deliveryVariance'" :class="{ 'delivery-variance-alert': isDeliveryVarianceAlert(scope.row) }" >{{ getTrackingColumnDisplayValue(scope.row, item.columnProp) }} +
+ + + + + {{ getCostManualDisplay(scope.row, item.columnProp) }} +
+ {{ formatCostAmount(getCostCalculatedValue(scope.row, item.columnProp)) }} {{ getTrackingColumnDisplayValue(scope.row, item.columnProp) }} @@ -917,7 +974,7 @@ - + { @@ -4387,10 +4585,13 @@ export default { // 列表会整表换行数据,未提交的工序日期编辑态不能带到新行上。 this.clearProcessDateEditClickTimer() this.exitProcessDateEdit() + this.exitCostEdit() searchProofTracking(inData).then(({ data }) => { this.dataListLoading = false this.resetProcessTooltip() if (data && data.code === 0) { + this.usdCnyExchangeRate = data.usdCnyExchangeRate == null ? null : data.usdCnyExchangeRate + this.usdCnyExchangeRateDraft = this.usdCnyExchangeRate == null ? '' : this.formatCostDisplay(this.usdCnyExchangeRate) this.dataList = (data.page && data.page.list) || [] this.syncDataListProcessValues() this.resetDataListPageOrderIndexes() @@ -4766,6 +4967,438 @@ export default { this.$message.error('Comments保存异常') }) }, + isCostManualColumn (columnProp) { + return COST_MANUAL_COLUMNS.indexOf(columnProp) > -1 + }, + isCostCalculatedColumn (columnProp) { + return COST_CALCULATED_COLUMNS.indexOf(columnProp) > -1 + }, + isCostEditing (row, columnProp) { + return !!row && this.costEditingKey === `${row.trackingId}:${columnProp}` + }, + isCostEditingRow (row) { + if (!row || !this.costEditingKey) { + return false + } + return this.costEditingKey.indexOf(`${row.trackingId}:`) === 0 + }, + formatCostDisplay (value) { + if (value == null || String(value).trim() === '') { + return '' + } + const number = Number(value) + if (!Number.isFinite(number)) { + return String(value) + } + return number.toFixed(6).replace(/\.?0+$/, '') + }, + // 费用金额固定两位。汇率仍走 formatCostDisplay,避免把 6.685421 收成 6.69。 + formatCostAmount (value) { + if (value == null || String(value).trim() === '') { + return '' + } + const number = Number(value) + if (!Number.isFinite(number)) { + return String(value) + } + return this.roundCost(number).toFixed(2) + }, + roundCost (value) { + const number = Number(value) + if (!Number.isFinite(number)) { + return null + } + return Math.round((number + Number.EPSILON) * 100) / 100 + }, + toCostNumber (value) { + if (value == null || String(value).trim() === '') { + return null + } + const number = Number(value) + return Number.isFinite(number) ? number : null + }, + getRowSampleQty (row) { + if (!row) { + return null + } + const qty = row.sampleQty != null && row.sampleQty !== '' ? row.sampleQty : row.proofingNumber + return this.toCostNumber(qty) + }, + getCostRateNumber () { + const rate = this.toCostNumber(this.usdCnyExchangeRate) + return rate != null && rate > 0 ? rate : null + }, + getCostManualDisplay (row, columnProp) { + if (!row) { + return '' + } + if (columnProp === 'customizedForMasAls') { + return this.isBlankValue(row.customizedForMasAls) ? '' : String(row.customizedForMasAls) + } + return this.formatCostAmount(row[columnProp]) + }, + normalizeCostDraft (columnProp, rawValue) { + if (columnProp === 'customizedForMasAls') { + const text = rawValue == null ? '' : String(rawValue).trim() + if (!text) { + return { value: null, error: '' } + } + if (text.length > 100) { + return { value: null, error: 'Customized for MAS/ALS不能超过100个字符' } + } + return { value: text, error: '' } + } + const text = rawValue == null ? '' : String(rawValue).trim() + if (!text) { + return { value: null, error: '' } + } + if (!/^\d+(\.\d+)?$/.test(text)) { + return { value: null, error: '请输入不小于0的数字' } + } + const number = Number(text) + if (!Number.isFinite(number) || number >= 1000000000000) { + return { value: null, error: '数值超出可保存范围' } + } + return { value: this.roundCost(number), error: '' } + }, + isSameCostDraft (columnProp, normalizedValue, originalDisplay) { + if (columnProp === 'customizedForMasAls') { + const left = normalizedValue == null ? '' : String(normalizedValue).trim() + const right = originalDisplay == null ? '' : String(originalDisplay).trim() + return left === right + } + return this.formatCostAmount(normalizedValue) === this.formatCostAmount(originalDisplay) + }, + sumPreviewCost (left, right) { + if (left == null && right == null) { + return null + } + return this.roundCost((left || 0) + (right || 0)) + }, + // 列表默认显示已保存结果。只有正在改的那个数字,才按当天汇率预览受影响的计算列。 + getCostCalculatedValue (row, columnProp) { + if (!row) { + return null + } + if (!this.isCostEditingRow(row)) { + return row[columnProp] + } + const editingProp = this.costEditingKey.slice(this.costEditingKey.indexOf(':') + 1) + const preview = this.previewEditedCost(row, editingProp) + if (preview && Object.prototype.hasOwnProperty.call(preview, columnProp)) { + return preview[columnProp] + } + return row[columnProp] + }, + previewEditedCost (row, editingProp) { + const normalized = this.normalizeCostDraft(editingProp, this.costEditDraft) + if (normalized.error) { + return null + } + const rate = this.getCostRateNumber() + if (editingProp === 'tpKctUsd') { + const qty = this.getRowSampleQty(row) + const sampleCostUsd = normalized.value == null || qty == null + ? null + : this.roundCost(normalized.value * qty / 1000) + return { + sampleCostUsd: sampleCostUsd, + sampleAndToolingCostUsd: this.sumPreviewCost(sampleCostUsd, this.toCostNumber(row.toolingCostUsd)) + } + } + if (editingProp === 'toolingCostCn') { + const toolingCostUsd = normalized.value == null || rate == null + ? null + : this.roundCost(normalized.value / rate) + return { + toolingCostUsd: toolingCostUsd, + sampleAndToolingCostUsd: this.sumPreviewCost(this.toCostNumber(row.sampleCostUsd), toolingCostUsd) + } + } + if (editingProp === 'sampleCostFromFnCn') { + return { + sampleCostFromFnUsd: normalized.value == null || rate == null + ? null + : this.roundCost(normalized.value / rate) + } + } + return null + }, + getCostCalculatedTitle (row, columnProp) { + const rate = this.formatCostDisplay(this.usdCnyExchangeRate) + const rateText = rate ? `汇率 ${rate}` : '未设置汇率,不能折算' + if (columnProp === 'sampleCostUsd') { + return '修改 TP 时按 TP(KCT/USD) × Sample Qty / 1000 计算' + } + if (columnProp === 'toolingCostUsd') { + return `修改 Tooling cost/CN 时按人民币 / ${rateText}` + } + if (columnProp === 'sampleAndToolingCostUsd') { + return 'Sample Cost/USD + Tooling Cost/USD。点“按汇率重算”会用当前汇率重算全部自动计算列' + } + if (columnProp === 'sampleCostFromFnUsd') { + return `修改 Sample Cost from FN/CN 时按人民币 / ${rateText}` + } + return '' + }, + normalizeExchangeRateDraft (rawValue) { + const text = rawValue == null ? '' : String(rawValue).trim() + if (!text) { + return { value: null, error: '' } + } + if (!/^\d+(\.\d{1,6})?$/.test(text)) { + return { value: null, error: '汇率请输入大于0的数字,最多6位小数' } + } + const number = Number(text) + if (!Number.isFinite(number) || number <= 0 || number >= 1000) { + return { value: null, error: '汇率必须大于0且小于1000' } + } + return { value: Math.round((number + Number.EPSILON) * 1000000) / 1000000, error: '' } + }, + isSameExchangeRate (left, right) { + if (left == null || right == null) { + return left == null && right == null + } + return Math.abs(Number(left) - Number(right)) < 0.0000005 + }, + // 汇率写在 siteid=2。空值允许保存,但之后不能把人民币折成美元。 + saveExchangeRate () { + const normalized = this.normalizeExchangeRateDraft(this.usdCnyExchangeRateDraft) + if (normalized.error) { + this.$message.warning(normalized.error) + return Promise.resolve(false) + } + if (this.isSameExchangeRate(normalized.value, this.toCostNumber(this.usdCnyExchangeRate))) { + return Promise.resolve(true) + } + if (this._exchangeRateSavePromise) { + return this._exchangeRateSavePromise + } + this.costRateSaving = true + this._exchangeRateSavePromise = saveProofTrackingExchangeRate({ + usdCnyRate: normalized.value, + updateBy: this.$store.state.user.name + }).then(({ data }) => { + if (!data || data.code !== 0) { + this.$message.error((data && data.msg) || '汇率保存失败') + return false + } + this.usdCnyExchangeRate = data.usdCnyExchangeRate == null ? null : data.usdCnyExchangeRate + this.usdCnyExchangeRateDraft = this.usdCnyExchangeRate == null ? '' : this.formatCostDisplay(this.usdCnyExchangeRate) + return true + }).catch(() => { + this.$message.error('汇率保存失败') + return false + }).finally(() => { + this.costRateSaving = false + this._exchangeRateSavePromise = null + }) + return this._exchangeRateSavePromise + }, + recalculateCostByRate () { + const normalized = this.normalizeExchangeRateDraft(this.usdCnyExchangeRateDraft) + if (normalized.error) { + this.$message.warning(normalized.error) + return + } + if (normalized.value == null) { + this.$message.warning('未设置汇率,无法计算') + return + } + this.$confirm('将按当前汇率重算全部自动计算的费用列并保存,是否继续?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + this.costRateRecalcLoading = true + return this.saveExchangeRate() + }).then(saved => { + if (saved !== true) { + return null + } + return refreshProofTrackingCostByRate({ updateBy: this.$store.state.user.name }) + }).then(response => { + if (!response) { + return + } + const data = response.data + if (data && data.code === 0) { + this.$message.success(data.msg || '费用已按汇率重算') + this.getDataList() + return + } + this.$message.error((data && data.msg) || '按汇率重算失败') + }).catch(error => { + if (error === 'cancel' || error === 'close') { + return + } + this.$message.error('按汇率重算失败') + }).finally(() => { + this.costRateRecalcLoading = false + }) + }, + resolveCostEditor () { + let editor = this.$refs.costEditor + if (Array.isArray(editor)) { + editor = editor.filter(item => item)[0] + } + return editor || null + }, + focusCostEditor (retried) { + const editor = this.resolveCostEditor() + if (!editor) { + if (!retried) { + this.$nextTick(() => { + this.focusCostEditor(true) + }) + } + return + } + if (typeof editor.focus === 'function') { + editor.focus() + } + if (typeof editor.toggleMenu === 'function' && !editor.visible) { + editor.toggleMenu() + } + const input = editor.$refs && editor.$refs.input + if (input && typeof input.select === 'function' && typeof editor.toggleMenu !== 'function') { + input.select() + } + }, + findCostEditingTarget () { + if (!this.costEditingKey) { + return null + } + const splitIndex = this.costEditingKey.indexOf(':') + if (splitIndex < 0) { + return null + } + const trackingId = this.costEditingKey.slice(0, splitIndex) + const columnProp = this.costEditingKey.slice(splitIndex + 1) + const row = (this.dataList || []).find(item => item && String(item.trackingId) === String(trackingId)) + if (!row) { + return null + } + return { row: row, columnProp: columnProp } + }, + exitCostEdit () { + this.costEditingKey = '' + this.costEditDraft = '' + this.costEditOriginal = '' + this.costSelectReady = false + }, + cancelCostEdit (row) { + if (row && !this.isCostEditingRow(row)) { + return + } + this.exitCostEdit() + }, + startCostEdit (row, columnProp) { + if (!row || !row.trackingId || !this.isCostManualColumn(columnProp)) { + return + } + const nextKey = `${row.trackingId}:${columnProp}` + if (this.costEditingKey === nextKey) { + return + } + if (this.costEditingKey) { + const editing = this.findCostEditingTarget() + if (editing) { + this.saveCostEdit(editing.row, editing.columnProp) + } else { + this.exitCostEdit() + } + } + this.costEditingKey = nextKey + this.costEditOriginal = this.getCostManualDisplay(row, columnProp) + this.costEditDraft = this.costEditOriginal + this.costSelectReady = false + this.$nextTick(() => { + this.focusCostEditor() + // 下拉刚挂上时会先关一次面板,这段时间不把关闭当成保存。 + setTimeout(() => { + if (this.costEditingKey === nextKey) { + this.costSelectReady = true + } + }, 200) + }) + }, + handleCostSelectVisible (visible, row, columnProp) { + if (visible || !this.isCostEditing(row, columnProp) || !this.costSelectReady) { + return + } + const editor = this.resolveCostEditor() + const typed = editor && editor.query != null ? String(editor.query).trim() : '' + if (typed && typed !== String(this.costEditDraft || '').trim()) { + this.costEditDraft = typed + } + this.saveCostEdit(row, columnProp) + }, + saveCostEdit (row, columnProp) { + if (!this.isCostEditing(row, columnProp)) { + return + } + const normalized = this.normalizeCostDraft(columnProp, this.costEditDraft) + if (normalized.error) { + this.$message.warning(normalized.error) + return + } + if (this.isSameCostDraft(columnProp, normalized.value, this.costEditOriginal)) { + this.exitCostEdit() + return + } + const version = (row._costSaveVersion || 0) + 1 + this.$set(row, '_costSaveVersion', version) + this.$set(row, columnProp, normalized.value) + const payload = { + trackingId: row.trackingId, + customizedForMasAls: this.isBlankValue(row.customizedForMasAls) ? null : String(row.customizedForMasAls).trim(), + tpKctUsd: this.toCostNumber(row.tpKctUsd), + toolingCostCn: this.toCostNumber(row.toolingCostCn), + sampleCostFromFnCn: this.toCostNumber(row.sampleCostFromFnCn), + updateBy: this.$store.state.user.name + } + this.exitCostEdit() + updateProofTrackingCost(payload).then(({ data }) => { + const current = (this.dataList || []).find(item => item && item.trackingId === row.trackingId) + if (!current || (current._costSaveVersion != null && current._costSaveVersion !== version)) { + return + } + if (data && data.code === 0) { + this.applySavedCost(current, data.row || {}) + if (data.msg && String(data.msg).indexOf('未设置汇率') > -1) { + this.$message.warning(data.msg) + } + return + } + this.$message.error((data && data.msg) || '费用保存失败') + this.getDataList() + }).catch(() => { + const current = (this.dataList || []).find(item => item && item.trackingId === row.trackingId) + if (!current || (current._costSaveVersion != null && current._costSaveVersion !== version)) { + return + } + this.$message.error('费用保存异常') + this.getDataList() + }) + }, + applySavedCost (row, saved) { + const fields = [ + 'customizedForMasAls', + 'tpKctUsd', + 'toolingCostCn', + 'sampleCostFromFnCn', + 'sampleCostUsd', + 'toolingCostUsd', + 'sampleAndToolingCostUsd', + 'sampleCostFromFnUsd' + ] + fields.forEach(field => { + if (saved && Object.prototype.hasOwnProperty.call(saved, field)) { + this.$set(row, field, saved[field]) + } + }) + }, handleTrackingTableSortChange (sortData) { this.trackingTableSortProp = sortData && sortData.prop ? String(sortData.prop) : '' this.trackingTableSortOrder = sortData && sortData.order ? String(sortData.order) : '' @@ -5727,8 +6360,7 @@ export default { baseline: current.baseline || '', pmInqueryTime: this.formatDate(current.pmInqueryTime), importance: this.normalizeImportanceValue(current.importance), - needDate: project.needDate || part.needDate || current.needDate || '', - remark: part.remark || project.remark || current.remark || '' + needDate: project.needDate || part.needDate || current.needDate || '' }) form.projectManagerName = this.getRoleDisplayName(form.projectManager) form.projectOwnerName = this.getRoleDisplayName(form.projectOwner) @@ -5855,9 +6487,9 @@ export default { : '' const nextStatus = inData.proofingStatus ? String(inData.proofingStatus).trim() : '' const proofingStatusChanged = this.oneKeyDialogMode !== 'create' && nextStatus !== previousStatus - if (proofingStatusChanged && this.proofingStatusChangeOptions.indexOf(nextStatus) === -1) { + if (proofingStatusChanged && !this.isAllowedOneKeyProofingStatusChange(previousStatus, nextStatus)) { this.saveOneKeyLoading = false - this.$message.warning('打样状态只能更改为终止、取消、报废') + this.$message.warning('打样状态只能更改为终止、取消、报废;当前为终止、取消、报废时可以改回进行中') return } if (proofingStatusChanged) { @@ -6483,7 +7115,23 @@ export default { return '#046e97' }, isProofingClosedStatus (row) { - return this.proofingStatusChangeOptions.indexOf(this.getNormalizedProofingStatus(row)) > -1 + return this.isClosedProofingStatusValue(this.getNormalizedProofingStatus(row)) + }, + isClosedProofingStatusValue (status) { + const normalizedStatus = status ? String(status).trim() : '' + return this.proofingStatusChangeOptions.indexOf(normalizedStatus) > -1 + }, + isAllowedOneKeyProofingStatusChange (previousStatus, nextStatus) { + const previous = previousStatus ? String(previousStatus).trim() : '' + const next = nextStatus ? String(nextStatus).trim() : '' + if (!next || next === previous) { + return true + } + if (this.proofingStatusChangeOptions.indexOf(next) > -1) { + return true + } + // 重新打开只允许从终止/取消/报废回到进行中,避免草稿等状态被改成进行中。 + return next === '进行中' && this.isClosedProofingStatusValue(previous) }, canDeleteProofStatus (row) { return this.deletableProofingStatuses.indexOf(this.getNormalizedProofingStatus(row)) > -1 @@ -7630,6 +8278,50 @@ export default { min-width: 28px; } +.cost-rate-tip { + display: inline-flex; + align-items: center; + margin-left: 10px; + color: #606266; + font-size: 12px; + line-height: 18px; + white-space: nowrap; +} + +.cost-rate-input { + width: 120px; + margin-left: 6px; +} + +.cost-cell { + min-height: 24px; +} + +.cost-display--editable { + display: inline-block; + width: 100%; + min-height: 24px; + line-height: 24px; + padding: 0 4px; + border: 1px solid transparent; + border-radius: 4px; + cursor: pointer; + box-sizing: border-box; +} + +.cost-display--editable:hover { + border-color: #dcdfe6; + background: #fff; +} + +.el-table.proof-tracking-table .cost-editor { + width: 100%; +} + +.cost-calculated-text { + color: #606266; +} + /* Comments 两行共 28px,与原单元格内容高度一致,不撑高整行 */ .el-table.proof-tracking-table .comments-text { display: -webkit-box;