From 8d9fe998c05590a94872cf85a49bcd08556e5243 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Thu, 22 Oct 2026 15:41:01 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E8=BF=9B=E8=A1=8C=E4=B8=AD?= =?UTF-8?q?=E9=85=8D=E8=BD=BD=E5=8F=AF=E7=BC=96=E8=BE=91=E3=80=81=E6=B1=87?= =?UTF-8?q?=E7=A5=A8=E9=80=89=E6=8B=A9=E4=B8=8E=E6=94=B6=E6=AC=BE=E6=96=B9?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E4=BC=98=E5=8C=96=EF=BC=8C=E5=B9=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=9A=E5=8A=A1=E9=83=A8=E9=97=A8=E4=B8=8E=E5=BE=85?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E5=9B=9E=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/api/business/waybill-manage.js | 7 + src/api/system/dept.js | 7 + src/option/business/common.js | 1 + src/option/business/waybill-manage.js | 20 +- .../components/waybill-manage-page.vue | 40 ++- src/views/business/contract-manage-change.vue | 334 ++++++++++++++++-- src/views/business/loading-manage.vue | 316 +++++++++++++++-- src/views/business/project-apply.vue | 107 +++++- src/views/payment/bill-payment-form.vue | 2 +- .../payment/payment-application-form.vue | 23 +- src/views/system/dept.vue | 24 ++ 11 files changed, 783 insertions(+), 98 deletions(-) diff --git a/src/api/business/waybill-manage.js b/src/api/business/waybill-manage.js index ac31975..eb66cb3 100644 --- a/src/api/business/waybill-manage.js +++ b/src/api/business/waybill-manage.js @@ -14,6 +14,13 @@ export const cancel = api.cancel; export const reassign = api.reassign; export const complete = api.complete; export const batchComplete = api.batchComplete; +/** 确认回单:待确认回单运单完成后转为已完成 */ +export const confirmReturn = id => + request({ + url: `${baseUrl}/confirm-return`, + method: 'post', + params: { id }, + }); export const changeRoute = data => request({ url: `${baseUrl}/change-route`, diff --git a/src/api/system/dept.js b/src/api/system/dept.js index c7f22de..2964557 100644 --- a/src/api/system/dept.js +++ b/src/api/system/dept.js @@ -139,3 +139,10 @@ export const getPlatformCompanySelect = () => { method: 'get', }); }; + +export const getBusinessDeptSelect = () => { + return request({ + url: '/blade-system/dept/business-dept-select', + method: 'get', + }); +}; diff --git a/src/option/business/common.js b/src/option/business/common.js index 8221c47..c142601 100644 --- a/src/option/business/common.js +++ b/src/option/business/common.js @@ -42,6 +42,7 @@ export const waybillStatusOptions = [ { label: '草稿', value: 'draft' }, { label: '待执行', value: 'pending' }, { label: '进行中', value: 'running' }, + { label: '待确认回单', value: 'pending_return' }, { label: '已完成', value: 'completed' }, { label: '已取消', value: 'cancelled' }, ]; diff --git a/src/option/business/waybill-manage.js b/src/option/business/waybill-manage.js index 9f617d3..9fd695b 100644 --- a/src/option/business/waybill-manage.js +++ b/src/option/business/waybill-manage.js @@ -478,7 +478,15 @@ export const config = { formatStatus(row, prop, defaultText) { if (prop !== 'businessStatus') return defaultText; const status = String(row.businessStatus || ''); - const terminalStatuses = ['draft', 'completed', 'cancelled', 'waiting_dispatch', 'dispatching']; + const terminalStatuses = [ + 'draft', + 'completed', + 'cancelled', + 'waiting_dispatch', + 'dispatching', + 'pending_return', + ]; + if (status === 'pending_return') return '待确认回单'; const needAccept = row.requireAccept === true || row.requireAccept === 1 || @@ -497,14 +505,20 @@ export const config = { getDriverRejectReason, isDriverRejectedWaybill, canEdit(row) { - // 进行中的运单不允许编辑 + // 进行中、待确认回单不允许编辑 if (isInProgressStatus(row, row.businessStatusName)) { return false; } + if (String(row.businessStatus || '') === 'pending_return') { + return false; + } return !isDriverRejectedWaybill(row); }, canCancel(row) { - // 进行中的运单不允许取消 + // 进行中、待确认回单不允许取消 + if (String(row.businessStatus || '') === 'pending_return') { + return false; + } return !isInProgressStatus(row, row.businessStatusName); }, deleteStatus: ['draft'], diff --git a/src/views/business/components/waybill-manage-page.vue b/src/views/business/components/waybill-manage-page.vue index dad89a3..e7b489f 100644 --- a/src/views/business/components/waybill-manage-page.vue +++ b/src/views/business/components/waybill-manage-page.vue @@ -1591,8 +1591,8 @@ 取消 - - {{ completeActionLabel }} + + {{ completeActionLabelFor(row) }} 维护里程 @@ -5111,7 +5111,29 @@ export default { return false; } const status = this.statusValue(row); - return status === 'processing' || status === 'running'; + return status === 'processing' || status === 'running' || status === 'pending_return'; + }, + completeActionLabelFor(row) { + return this.statusValue(row) === 'pending_return' ? '确认回单' : this.completeActionLabel; + }, + handleCompleteAction(row) { + const isPendingReturn = this.statusValue(row) === 'pending_return'; + const actionName = this.completeActionLabelFor(row); + this.$confirm(`确定${actionName}该${this.config.title}?`, '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning', + }) + .then(() => { + if (isPendingReturn && typeof this.api.confirmReturn === 'function') { + return this.api.confirmReturn(row.id); + } + return this.api.complete(row.id); + }) + .then(() => { + this.$message.success(`${actionName}成功`); + this.onLoad(this.page, this.query); + }); }, canMaintainMileage(row) { return ( @@ -6543,6 +6565,9 @@ export default { if (status === 'completed') { return 'primary'; } + if (status === 'pending_return') { + return 'warning'; + } return 'warning'; }, normalizeRow(row, saveAsDraft = false) { @@ -10356,7 +10381,14 @@ export default { this.$message.warning('请选择至少一条数据'); return; } - this.$confirm('确定完成选中的运单?', '提示', { + const pendingReturnCount = this.selectionList.filter( + row => this.statusValue(row) === 'pending_return' + ).length; + const tip = + pendingReturnCount > 0 + ? `确定完成选中的运单?其中 ${pendingReturnCount} 条为待确认回单,确认后将转为已完成。` + : '确定完成选中的运单?'; + this.$confirm(tip, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', diff --git a/src/views/business/contract-manage-change.vue b/src/views/business/contract-manage-change.vue index c745705..63f5f88 100644 --- a/src/views/business/contract-manage-change.vue +++ b/src/views/business/contract-manage-change.vue @@ -8,11 +8,17 @@ + - + @@ -80,6 +87,7 @@ format="YYYY-MM-DD" value-format="YYYY-MM-DD" placeholder="请输入" + :disabled="isSettlementRuleChange" /> 至 @@ -95,6 +104,7 @@ @@ -105,22 +115,36 @@ :model-value="contractAmountDisplay" placeholder="请输入" clearable + :disabled="isSettlementRuleChange" @input="handleContractAmountInput" @blur="formatContractAmountField" @clear="form.contractAmount = null" /> - + - + - + @@ -129,13 +153,19 @@ - + - + - + - + @@ -201,6 +248,7 @@ maxlength="200" show-word-limit placeholder="请输入其他事项说明" + :disabled="isSettlementRuleChange" /> @@ -209,6 +257,7 @@ title="用印材料" description attachment-type + :readonly="isSettlementRuleChange" :attachment-location-options="changeAttachmentLocationOptions" :rows="contractFileRows" :preview="previewAttachment" @@ -219,12 +268,36 @@
计费信息
- 添加
- 系统生成账单导入生成 - + 系统生成账单导入生成 +
@@ -243,19 +316,106 @@ 关闭 - - - - - - + + + + + + - + - - - - + + + +
@@ -263,17 +423,48 @@
付款比例设置 - +
- - - + + + - 添加 + 添加 this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; }, handleAttachmentUploadToLocation({ location, rows = [] }) { if (!rows.length) return; + if (this.isSettlementRuleChange) { + if (location === '变更材料') { + this.changeMaterials = [...(this.changeMaterials || []), ...rows]; + } + return; + } if (isContractSealMaterialLocation(location)) { this.contractFileRows = [...(this.contractFileRows || []), ...rows]; return; @@ -743,7 +947,81 @@ export default { removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); }, validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; }, validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; }, - async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', bizId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected', mkEnabled: await loadMkEnabled().catch(() => false) }); this.$message.success('变更已提交'); this.$router.back(); }, + async submit() { + if (this.form.changeType === '终止合同') { + await api.terminate(this.form.id, this.form.changeReason); + this.$message.success('合同已终止'); + this.$router.back(); + return; + } + await this.$refs.formRef.validate(); + if (!this.isSettlementRuleChange) { + const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); + if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { + this.$message.warning('付款比例上限合计必须等于100%'); + return; + } + } + if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; + else this.formalSettlementConfig = { ...this.settlementRule }; + if ( + !this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || + !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置') + ) { + return; + } + const settlementRule = { + preSettlementConfig: this.preSettlementConfig, + formalSettlementConfig: this.formalSettlementConfig, + }; + const payload = this.isSettlementRuleChange + ? { + id: this.form.id, + changeType: this.form.changeType, + changeContent: this.form.changeContent, + changeReason: this.form.changeReason, + feeGenerationMode: this.feeGenerationMode, + billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, + billingPlanJson: JSON.stringify(this.plans), + settlementRuleJson: JSON.stringify(settlementRule), + preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), + formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), + changeAttachmentsJson: JSON.stringify(this.changeMaterials), + } + : { + ...this.form, + changeType: this.form.changeType || '合同信息变更', + settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', + copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), + invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), + paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), + contractAmount: normalizeOptionalAmount(this.form.contractAmount), + startDate: this.period[0], + endDate: this.period[1], + feeGenerationMode: this.feeGenerationMode, + billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, + billingPlanJson: JSON.stringify(this.plans), + settlementRuleJson: JSON.stringify(settlementRule), + preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), + formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), + paymentRatioJson: JSON.stringify(this.paymentRatioRows), + contractFileJson: JSON.stringify(this.contractFileRows), + attachmentsJson: JSON.stringify(this.attachments), + changeContent: this.form.changeContent, + changeReason: this.form.changeReason, + changeAttachmentsJson: JSON.stringify(this.changeMaterials), + }; + await api.submitChange(payload); + await submitMkApprovalFlow({ + bizType: 'contract-manage', + bizId: this.form.id, + subjectName: this.form.contractName || '', + approvalStatus: this.form.approvalStatus || 'change_rejected', + mkEnabled: await loadMkEnabled().catch(() => false), + }); + this.$message.success('变更已提交'); + this.$router.back(); + }, }, }; diff --git a/src/views/business/loading-manage.vue b/src/views/business/loading-manage.vue index aac814e..ce1374d 100644 --- a/src/views/business/loading-manage.vue +++ b/src/views/business/loading-manage.vue @@ -1357,19 +1357,29 @@ - @@ -2107,6 +2152,8 @@ export default { routeNodes: [], cargoRows: [], routeDragIndex: -1, + /** 进行中编辑:子运单打卡记录映射 waybillId -> records[] */ + waybillPunchMap: {}, transportTypeOptions: [], projectOptions: [], customerOptions: [], @@ -2170,6 +2217,23 @@ export default { dialogReadonly() { return this.dialogMode === 'view'; }, + /** 进行中配载编辑:可加子单/调未打卡路线,但任务必填字段锁定 */ + isRunningEdit() { + return ( + this.dialogMode === 'edit' && + String(this.dialogForm.businessStatus || '') === 'running' + ); + }, + /** 进行中任务必填字段(已派车)不可改 */ + isRunningTaskRequiredLocked() { + return this.isRunningEdit; + }, + /** 自运进行中:司机/手机号同属必填派车信息,不可改 */ + isRunningSelfDriverLocked() { + return ( + this.isRunningEdit && String(this.dialogForm.carrierType || '').trim() === '自运' + ); + }, // 打卡时间轴:已打卡按时间升序,未打卡保持原节点顺序排在其后 orderedPunchRecords() { const records = Array.isArray(this.punchRecords) ? this.punchRecords : []; @@ -2913,6 +2977,7 @@ export default { if (status === 'running') { return [ { type: 'view', label: '详情' }, + { type: 'edit', label: '编辑' }, { type: 'complete', label: '完成' }, ]; } @@ -3014,7 +3079,12 @@ export default { query: { mode, ...(mode === 'edit' && row?.id ? { id: row.id } : {}), - name: mode === 'edit' ? '编辑配载管理' : '新增配载管理', + name: + mode === 'edit' + ? row?.businessStatus === 'running' + ? '编辑进行中配载' + : '编辑配载管理' + : '新增配载管理', }, }); return; @@ -3037,6 +3107,9 @@ export default { this.dialogForm.loadingSubNos ); this.syncProcessWaybillOptions(); + if (this.isRunningEdit) { + await this.refreshWaybillPunchMap(); + } this.restoreRouteAndCargo(); this.restoreRouteChangeRecords(); this.loadPunchRecords(); @@ -3071,6 +3144,8 @@ export default { this.taskCarrierLoading = false; this.routeNodes = []; this.cargoRows = []; + this.routeDragIndex = -1; + this.waybillPunchMap = {}; this.candidatePage.currentPage = 1; this.candidatePage.total = 0; this.candidateSearchVisible = false; @@ -3435,20 +3510,144 @@ export default { const statusName = String(row.businessStatusName || row.statusName || ''); return statusName.includes('已完成'); }, + isPunchRecordDone(record = {}) { + if (record.punched === false || record.statusName === '未打卡') return false; + return Boolean(record.punched || record.punchTime || record.punchAt || record.punchDateTime); + }, + isLoadPunchRecord(record = {}) { + if (!this.isPunchRecordDone(record)) return false; + const code = String(record.nodeCode || '') + .trim() + .toLowerCase(); + const name = String(record.nodeName || '').trim(); + return code === 'load' || name.includes('装货'); + }, + isUnloadPunchRecord(record = {}) { + if (!this.isPunchRecordDone(record)) return false; + const code = String(record.nodeCode || '') + .trim() + .toLowerCase(); + const name = String(record.nodeName || '').trim(); + return ( + code === 'unload' || + name.includes('卸货') || + code === 'arrival' || + name.includes('到货') || + code === 'sign' || + name.includes('签收') + ); + }, + waybillPunchRecords(row = {}) { + const key = String(row?.id || ''); + if (!key) return []; + const records = this.waybillPunchMap[key]; + return Array.isArray(records) ? records : []; + }, + isWaybillLoadPunched(row = {}) { + return this.waybillPunchRecords(row).some(record => this.isLoadPunchRecord(record)); + }, + isWaybillUnloadPunched(row = {}) { + return this.waybillPunchRecords(row).some(record => this.isUnloadPunchRecord(record)); + }, + isWaybillPunchLocked(row = {}) { + return this.isWaybillLoadPunched(row) || this.isWaybillUnloadPunched(row); + }, + isSelectedWaybillRemoveLocked(row = {}) { + if (this.isWaybillCompleted(row)) return true; + return this.isRunningEdit && this.isWaybillPunchLocked(row); + }, + isRouteNodeTagLocked(tag, rows = this.selectedWaybillRows) { + const match = String(tag || '').match(/^(装|卸)(\d+)$/); + if (!match) return false; + const row = rows[Number(match[2]) - 1]; + if (!row) return false; + if (this.isRunningEdit) { + return match[1] === '装' ? this.isWaybillLoadPunched(row) : this.isWaybillUnloadPunched(row); + } + return this.isWaybillCompleted(row); + }, isRouteChangeNodeLocked(node = {}) { return Boolean(node?.locked); }, syncRouteNodeLockedFlags(nodes = [], rows = this.selectedWaybillRows) { return (nodes || []).map(node => { - const locked = (node.tags || []).some(tag => { - const match = String(tag).match(/^(装|卸)(\d+)$/); - if (!match) return false; - const row = rows[Number(match[2]) - 1]; - return row ? this.isWaybillCompleted(row) : false; - }); + const locked = (node.tags || []).some(tag => this.isRouteNodeTagLocked(tag, rows)); return { ...node, locked }; }); }, + async refreshWaybillPunchMap() { + const rows = (this.selectedWaybillRows || []).filter(row => row?.id); + if (!rows.length) { + this.waybillPunchMap = {}; + return; + } + const entries = await Promise.all( + rows.map(async row => { + try { + const res = await getWaybillPunchRecords(row.id); + const data = res?.data?.data || res?.data || {}; + return [String(row.id), Array.isArray(data.records) ? data.records : []]; + } catch (error) { + return [String(row.id), []]; + } + }) + ); + this.waybillPunchMap = Object.fromEntries(entries); + }, + /** + * 进行中编辑:已打卡地点保持在前且不可插入,新增地址只能排在已打卡节点之后。 + */ + mergeRouteNodesKeepingPunched(prevNodes = [], builtNodes = []) { + const prev = Array.isArray(prevNodes) ? prevNodes : []; + const built = Array.isArray(builtNodes) ? builtNodes : []; + if (!this.isRunningEdit) { + return built; + } + const builtByAddress = new Map(); + built.forEach(node => { + if (node?.address && !builtByAddress.has(node.address)) { + builtByAddress.set(node.address, node); + } + }); + const lockedAddresses = new Set( + built.filter(node => node?.locked && node.address).map(node => node.address) + ); + const used = new Set(); + const lockedResult = []; + prev.forEach(node => { + if (!node?.address || !lockedAddresses.has(node.address) || used.has(node.address)) { + return; + } + const latest = builtByAddress.get(node.address); + if (!latest) return; + lockedResult.push({ ...latest, locked: true }); + used.add(node.address); + }); + built.forEach(node => { + if (!node?.locked || !node.address || used.has(node.address)) return; + lockedResult.push({ ...node, locked: true }); + used.add(node.address); + }); + const unlockedResult = []; + prev.forEach(node => { + if (!node?.address || lockedAddresses.has(node.address) || used.has(node.address)) { + return; + } + const latest = builtByAddress.get(node.address); + if (!latest) return; + unlockedResult.push({ ...latest, locked: false }); + used.add(node.address); + }); + built.forEach(node => { + if (node?.locked || !node?.address || used.has(node.address)) return; + unlockedResult.push({ ...node, locked: false }); + used.add(node.address); + }); + return [...lockedResult, ...unlockedResult].map((node, index) => ({ + ...node, + key: `${node.address}-${index}`, + })); + }, openRouteChangeMapDialog(row, field) { if (this.isWaybillCompleted(row)) return; this.routeChangeMapTarget = { row, field }; @@ -3766,6 +3965,33 @@ export default { } }) ); + if (this.isRunningEdit && String(this.dialogForm.carrierType || '').trim() === '承运商') { + const contractId = this.dialogForm.carrierContractId; + if (contractId) { + const projectIds = [ + ...new Set( + [...this.selectedWaybillRows, ...selectedRows] + .map(row => row.projectId) + .filter(Boolean) + ), + ]; + if (projectIds.length) { + try { + const response = await loadingApi.getCarrierContracts(projectIds); + const contracts = response?.data?.data || response?.data || []; + const stillAllowed = (Array.isArray(contracts) ? contracts : []).some( + item => String(item.id) === String(contractId) + ); + if (!stillAllowed) { + ElMessage.warning('新增子运单须兼容当前配载单已选定的承运商合同'); + return; + } + } catch (error) { + // 合同校验失败时仍允许添加,最终由后端确认拦截 + } + } + } + } const exists = new Set( this.selectedWaybillRows.map(row => String(row.id || row.waybillNo)) ); @@ -3777,8 +4003,12 @@ export default { exists.add(key); } }); + const prevRouteNodes = [...this.routeNodes]; this.selectedWaybillRows = nextRows; - this.rebuildSummaryFromWaybills(); + if (this.isRunningEdit) { + await this.refreshWaybillPunchMap(); + } + this.rebuildSummaryFromWaybills(true, prevRouteNodes); await this.loadTaskCarrierOptions(); this.$refs.candidateTableRef?.clearSelection?.(); this.candidateSelection = []; @@ -3790,11 +4020,23 @@ export default { await this.addWaybillRows(this.candidateSelection); }, removeSelectedWaybill(index) { + const row = this.selectedWaybillRows[index]; + if (this.isSelectedWaybillRemoveLocked(row)) { + ElMessage.warning('已打卡的子运单不可移除'); + return; + } this.selectedWaybillRows.splice(index, 1); + if (this.isRunningEdit) { + this.refreshWaybillPunchMap().then(() => { + this.rebuildSummaryFromWaybills(); + this.loadTaskCarrierOptions(); + }); + return; + } this.rebuildSummaryFromWaybills(); this.loadTaskCarrierOptions(); }, - rebuildSummaryFromWaybills(rebuildRoute = true) { + rebuildSummaryFromWaybills(rebuildRoute = true, prevRouteNodes = null) { const rows = this.selectedWaybillRows; this.dialogForm.loadingSubNos = rows .map(row => row.waybillNo) @@ -3819,7 +4061,13 @@ export default { this.dialogForm.transportType = rows.find(row => row.transportType)?.transportType || ''; } if (rebuildRoute) { - this.routeNodes = this.buildRouteNodes(rows); + const builtNodes = this.buildRouteNodes(rows); + this.routeNodes = this.isRunningEdit + ? this.mergeRouteNodesKeepingPunched( + prevRouteNodes || this.routeNodes, + builtNodes + ) + : builtNodes; } this.cargoRows = this.buildCargoRows(rows); // 编辑回填阶段 routeNodes 尚未恢复时跳过,避免清空已有发到货地 @@ -3839,7 +4087,9 @@ export default { if (locked) node.locked = true; }; rows.forEach((row, rowIndex) => { - const locked = this.isWaybillCompleted(row); + const locked = this.isRunningEdit + ? this.isWaybillLoadPunched(row) + : this.isWaybillCompleted(row); const departure = row.departureAddress || row.departureName; appendNode( departure, @@ -3849,7 +4099,9 @@ export default { ); }); rows.forEach((row, rowIndex) => { - const locked = this.isWaybillCompleted(row); + const locked = this.isRunningEdit + ? this.isWaybillUnloadPunched(row) + : this.isWaybillCompleted(row); const arrival = row.arrivalAddress || row.arrivalName; appendNode( arrival, @@ -3901,7 +4153,9 @@ export default { if ( originalLockedKeys.some((key, lockedIndex) => key && key !== nextLockedKeys[lockedIndex]) ) { - ElMessage.warning('已完成运单对应的路线不可调整'); + ElMessage.warning( + this.isRunningEdit ? '已打卡地点不可调整,新地址只能在未打卡节点间拖拽' : '已完成运单对应的路线不可调整' + ); this.routeDragIndex = -1; return; } @@ -4145,15 +4399,15 @@ export default { try { const payload = { ...this.normalizePayload(), - // 确认生成正式配载单(待执行),覆盖草稿状态 - businessStatus: 'pending', + // 进行中编辑保持 running;其余确认生成正式配载单为待执行 + businessStatus: this.isRunningEdit ? 'running' : 'pending', }; if (this.dialogMode === 'reassign') { await loadingApi.reassign(payload); ElMessage.success('派单成功'); } else { await loadingApi.submit(payload); - ElMessage.success('确认成功'); + ElMessage.success(this.isRunningEdit ? '保存成功' : '确认成功'); } sessionStorage.setItem(loadingManageListRefreshKey, '1'); this.closeLoadingDialog(); @@ -4787,6 +5041,16 @@ export default { color: #909399; } + .loading-manage-dialog__route-hint { + color: #909399; + font-size: 12px; + } + + .loading-manage-dialog__locked-tip { + color: #909399; + font-size: 12px; + } + .loading-manage-dialog__route-type { width: 28px; height: 28px; diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue index e4b41bb..75dcdfe 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -921,7 +921,7 @@ import { getList as getCustomerArchiveList, getDetail as getCustomerArchiveDetail, } from '@/api/vehicle/customer-archive'; -import { getDeptTree, getPlatformCompanySelect } from '@/api/system/dept'; +import { getBusinessDeptSelect, getDeptTree, getPlatformCompanySelect } from '@/api/system/dept'; import { getDictionary as getSystemDictionary } from '@/api/system/dict'; import { getDictionary as getBizDictionary } from '@/api/system/dictbiz'; import { getList as getUserList } from '@/api/system/user'; @@ -1559,6 +1559,7 @@ export default { projectType: type === 'majorSupplement' ? '重大项目' : '普通项目', }); this.fillDefaultUsers(); + this.fillDefaultBusinessDept(); return; } if (!id) return; @@ -1891,6 +1892,7 @@ export default { .filter(Boolean); this.syncSelectedCustomerOptions(); this.ensureCurrentPlatformOption(); + this.ensureCurrentBusinessDeptOption(); }, fillDefaultUsers() { const userId = this.userInfo?.userId || this.userInfo?.id || ''; @@ -1909,6 +1911,25 @@ export default { this.form.principalUserName = userName; } }, + /** + * 新增/重大补录:业务部门默认取当前用户所属部门中第一个「是否业务部门=是」的组织。 + */ + fillDefaultBusinessDept() { + if (!['add', 'majorSupplement'].includes(this.dialogType)) return; + if (this.form.businessDeptId) return; + const raw = this.userInfo?.deptId || this.userInfo?.dept_id || ''; + const userDeptIds = String(raw) + .split(/[,,]/) + .map(item => String(item || '').trim()) + .filter(item => item && item !== '-1'); + if (!userDeptIds.length || !this.deptOptions.length) return; + const matched = userDeptIds.find(id => + this.deptOptions.some(item => String(item.value) === String(id)) + ); + if (!matched) return; + this.form.businessDeptId = matched; + this.handleBusinessDeptChange(matched); + }, resolveUserRealName(row = {}, role) { return ( row[`${role}RealName`] || @@ -2491,17 +2512,61 @@ export default { this.$refs.projectForm?.validateField('projectType'); }, loadDeptOptions() { - getDeptTree().then(res => { - const deptTree = res.data.data || []; - // 新增/编辑/重大补录/项目变更均可选择业务部门,排除外部组织 - const businessDeptTree = ['add', 'edit', 'majorSupplement', 'change'].includes(this.dialogType) - ? this.excludeExternalOrganization(deptTree) - : deptTree; - this.deptOptions = this.flattenDept(businessDeptTree); - this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree); - }); + Promise.all([getDeptTree(), getBusinessDeptSelect()]) + .then(([treeRes, bizRes]) => { + const deptTree = this.excludeExternalOrganization(treeRes.data.data || []); + const bizList = bizRes.data.data || []; + const bizIds = new Set( + bizList + .map(item => (item.id == null || item.id === '' ? '' : String(item.id))) + .filter(Boolean) + ); + // 仅保留「是否业务部门=是」的节点及其祖先,祖先不可选 + const businessDeptTree = this.filterBusinessDeptTree(deptTree, bizIds); + this.deptOptions = this.flattenDept(businessDeptTree).filter(item => + bizIds.has(String(item.value)) + ); + this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree, bizIds); + this.fillDefaultBusinessDept(); + this.ensureCurrentBusinessDeptOption(); + }) + .catch(() => { + this.deptOptions = []; + this.businessDeptTreeOptions = []; + }); this.loadPlatformCompanyOptions(); }, + /** + * 剪枝组织树:只保留业务部门节点及其祖先路径。 + */ + filterBusinessDeptTree(list = [], bizIds = new Set()) { + return (list || []).reduce((result, item) => { + const children = item.children?.length + ? this.filterBusinessDeptTree(item.children, bizIds) + : []; + const isBiz = bizIds.has(String(item.id)); + if (!isBiz && !children.length) { + return result; + } + result.push({ + ...item, + children: children.length ? children : undefined, + }); + return result; + }, []); + }, + ensureCurrentBusinessDeptOption() { + const id = this.form.businessDeptId; + if (!id) return; + const exists = this.deptOptions.some(item => String(item.value) === String(id)); + if (exists) return; + const label = this.form.businessDeptName || String(id); + this.deptOptions.unshift({ label, rawLabel: label, value: id }); + const pathExists = this.findDeptPath(id, this.businessDeptTreeOptions); + if (!pathExists) { + this.businessDeptTreeOptions.unshift({ label, value: id }); + } + }, loadPlatformCompanyOptions() { getPlatformCompanySelect().then(res => { const list = res.data.data || []; @@ -2545,12 +2610,22 @@ export default { return result; }, []); }, - toDeptCascaderOptions(list = []) { - return list.map(item => ({ - label: item.title || item.deptName || item.name, - value: item.id, - children: item.children?.length ? this.toDeptCascaderOptions(item.children) : undefined, - })); + toDeptCascaderOptions(list = [], bizIds = null) { + return list.map(item => { + const value = item.id == null || item.id === '' ? item.value : item.id; + const option = { + label: item.title || item.deptName || item.name || item.label, + value: value == null || value === '' ? '' : String(value), + children: item.children?.length + ? this.toDeptCascaderOptions(item.children, bizIds) + : undefined, + }; + // 仅业务部门可选;祖先节点保留路径但禁用 + if (bizIds && !bizIds.has(String(option.value))) { + option.disabled = true; + } + return option; + }); }, flattenDept(list = [], level = 0) { return list.reduce((result, item) => { diff --git a/src/views/payment/bill-payment-form.vue b/src/views/payment/bill-payment-form.vue index 77297ed..d848ffd 100644 --- a/src/views/payment/bill-payment-form.vue +++ b/src/views/payment/bill-payment-form.vue @@ -271,7 +271,7 @@ export default { this.unwrapData( await billLedgerApi.getAvailableOptions( this.billDialog.keyword, - this.form.deptId, + undefined, this.form.billLedgerId ) ) || []; diff --git a/src/views/payment/payment-application-form.vue b/src/views/payment/payment-application-form.vue index eb13204..2880a43 100644 --- a/src/views/payment/payment-application-form.vue +++ b/src/views/payment/payment-application-form.vue @@ -1229,14 +1229,6 @@ export default { callback(); return; } - if ( - selected.billType && - selected.billType !== 'received' && - !this.billMatchesSettlementParties(selected) - ) { - callback(new Error('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票')); - return; - } if (selected.maturityDate) { const maturity = this.$dayjs(selected.maturityDate).startOf('day'); const today = this.$dayjs().startOf('day'); @@ -2444,8 +2436,7 @@ export default { } }, payeeReceiptLabel(item) { - const name = item.accountName || item.accountHolderName || '未命名收款方'; - return item.bankAccount ? `${name}(${item.bankAccount})` : name; + return item.accountName || item.accountHolderName || '未命名收款方'; }, receiptAccountLabel(item) { return [item.accountName || item.accountHolderName, item.bankName, item.bankAccount] @@ -2603,7 +2594,7 @@ export default { this.unwrapData( await billLedgerApi.getAvailableOptions( keyword, - this.form.deptId, + undefined, selectedId, this.settlementPayerName, this.settlementPayeeName @@ -2628,7 +2619,7 @@ export default { this.billPage.current, this.billPage.size, String(this.billQuery.keyword || '').trim() || undefined, - this.form.deptId, + undefined, this.form.billLedgerId, this.settlementPayerName, this.settlementPayeeName @@ -2667,14 +2658,6 @@ export default { ); }, selectBill(row) { - if ( - row?.billType && - row.billType !== 'received' && - !this.billMatchesSettlementParties(row) - ) { - ElMessage.warning('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票'); - return; - } if (row?.maturityDate) { const maturity = this.$dayjs(row.maturityDate).startOf('day'); const today = this.$dayjs().startOf('day'); diff --git a/src/views/system/dept.vue b/src/views/system/dept.vue index a22b6f3..0b4033c 100644 --- a/src/views/system/dept.vue +++ b/src/views/system/dept.vue @@ -535,6 +535,27 @@ export default { }, ], }, + { + label: '是否业务部门', + prop: 'isBusinessDept', + type: 'select', + dicData: [ + { label: '否', value: 0 }, + { label: '是', value: 1 }, + ], + dataType: 'number', + value: 0, + width: 120, + order: 34, + formatter: row => (Number(row.isBusinessDept) === 1 ? '是' : '否'), + rules: [ + { + required: true, + message: '请选择是否业务部门', + trigger: 'change', + }, + ], + }, { label: '排序', prop: 'sort', @@ -1523,6 +1544,9 @@ export default { if (this.form.isPlatformCompany === undefined || this.form.isPlatformCompany === null) { this.form.isPlatformCompany = 0; } + if (this.form.isBusinessDept === undefined || this.form.isBusinessDept === null) { + this.form.isBusinessDept = 0; + } this.clearEmptyContractInitialSerial(this.form); this.initData(this.form.tenantId); this.loadParentDeptCode(this.form.parentId);