🔀 合并主干 master 到 dev

This commit is contained in:
2026-11-04 18:24:39 +08:00
52 changed files with 1339 additions and 549 deletions
@@ -12,12 +12,12 @@
<el-descriptions-item label="方案名称">{{
displayValue(draft.planName)
}}</el-descriptions-item>
<el-descriptions-item label="适用客商">{{
displayValue(draft.customerName || partyOptionLabel(draft.partyRowId))
}}</el-descriptions-item>
<el-descriptions-item label="运输方式">{{
transportModeLabel(draft.transportMode)
}}</el-descriptions-item>
<el-descriptions-item label="默认方案">{{
isDefaultPlan(draft) ? '是' : '否'
}}</el-descriptions-item>
<el-descriptions-item label="备注">{{ displayValue(draft.remark) }}</el-descriptions-item>
</el-descriptions>
<el-form
@@ -37,6 +37,20 @@
:disabled="readonly" /></el-form-item
></el-col>
<el-col :span="8"
><el-form-item label="适用客商" prop="partyRowId"
><el-select
v-model="draft.partyRowId"
clearable
filterable
placeholder="请选择适用客商"
@change="handlePartyChange"
><el-option
v-for="item in partyOptions"
:key="item.value"
:label="item.label"
:value="item.value" /></el-select></el-form-item
></el-col>
<el-col :span="8"
><el-form-item label="运输方式" prop="transportMode"
><el-select
v-model="draft.transportMode"
@@ -51,16 +65,6 @@
:label="item.label"
:value="item.value" /></el-select></el-form-item
></el-col>
<el-col :span="8"
><el-form-item
><el-checkbox v-model="draft.defaultPlan" :disabled="readonly">默认方案</el-checkbox
><el-tooltip content="同一运输方式仅支持配置一个默认计费方案" placement="top">
<el-icon class="billing-plan-editor__default-tip"
><InfoFilled
/></el-icon> </el-tooltip
></el-form-item
></el-col
>
<el-col :span="24"
><el-form-item label="备注"
><el-input
@@ -380,6 +384,7 @@ import { InfoFilled } from '@element-plus/icons-vue';
import { getDictionary } from '@/api/system/dictbiz';
import SectionCard from '@/components/section-card/main.vue';
import { currencyUnit } from '@/utils/currency';
// InfoFilled 仅用于保底计费重量列头提示
const clone = value => JSON.parse(JSON.stringify(value));
/** 计费要素与计量单位维度的对应关系(这些要素可选计费单位) */
@@ -433,6 +438,7 @@ export default {
index: { type: Number, default: -1 },
readonly: Boolean,
settlementCurrency: { type: String, default: 'RMB' },
partyOptions: { type: Array, default: () => [] },
},
emits: ['update:modelValue', 'save'],
data() {
@@ -476,6 +482,7 @@ export default {
cargoRequest: null,
formRules: {
planName: [{ required: true, message: '请输入方案名称', trigger: 'blur' }],
partyRowId: [{ required: true, message: '请选择适用客商', trigger: 'change' }],
transportMode: [{ required: true, message: '请选择运输方式', trigger: 'change' }],
},
};
@@ -516,10 +523,24 @@ export default {
handler(value) {
if (this.modelValue) {
this.draft = this.normalizePlan(value);
this.applyDefaultPartySelection();
this.loadRuleItems();
}
},
},
modelValue(visible) {
if (visible) {
this.$nextTick(() => this.applyDefaultPartySelection());
}
},
partyOptions: {
deep: true,
handler() {
if (this.modelValue) {
this.applyDefaultPartySelection();
}
},
},
},
methods: {
displayValue(value) {
@@ -539,27 +560,62 @@ export default {
);
return this.displayValue(option?.label || this.draft.transportModeLabel || value);
},
isDefaultPlan(plan) {
return (
plan?.defaultPlan === true ||
plan?.defaultPlan === 1 ||
['true', '1'].includes(String(plan?.defaultPlan).toLowerCase())
);
},
normalizePlan(value = {}) {
const plan = {
planName: '',
partyRowId: '',
partyRole: '',
customerId: '',
customerName: '',
transportMode: '',
transportModeLabel: '',
defaultPlan: false,
remark: '',
...clone(value || {}),
};
// 一个客商对应一个方案,不再维护默认方案字段
delete plan.defaultPlan;
plan.rules = (value?.rules?.length ? value.rules : [defaultRule()]).map(rule =>
this.normalizeRule(rule)
);
return plan;
},
partyOptionLabel(partyRowId) {
const matched = (this.partyOptions || []).find(
item => String(item.value) === String(partyRowId)
);
return matched?.label || '';
},
/**
* 仅一个可选客商时默认选中(客户合同仅一个甲方的常见场景)
*/
applyDefaultPartySelection() {
if (this.readonly) {
return;
}
const options = this.partyOptions || [];
if (options.length !== 1) {
return;
}
const current = String(this.draft.partyRowId || '');
const only = options[0];
const onlyValue = String(only?.value || only?.partyRowId || '');
if (!onlyValue) {
return;
}
if (current && current === onlyValue) {
return;
}
this.handlePartyChange(only.value || only.partyRowId);
},
handlePartyChange(partyRowId) {
const matched = (this.partyOptions || []).find(
item => String(item.value) === String(partyRowId)
);
this.draft.partyRowId = matched?.partyRowId || matched?.value || '';
this.draft.partyRole = matched?.partyRole || '';
this.draft.customerId = matched?.customerId || '';
this.draft.customerName = matched?.customerName || '';
},
normalizeRule(rule = {}) {
const next = { ...defaultRule(), ...clone(rule) };
if (next.billingType === '阶梯单价' && next.minimumBillingWeight === '') {
@@ -1032,6 +1088,11 @@ export default {
this.$refs.formRef.validate(valid => {
if (!valid || !this.validateRules()) return;
const plan = clone(this.draft);
this.handlePartyChange(plan.partyRowId);
plan.partyRowId = this.draft.partyRowId;
plan.partyRole = this.draft.partyRole;
plan.customerId = this.draft.customerId;
plan.customerName = this.draft.customerName;
const transportMode = this.transportModeOptions.find(
item => String(item.value) === String(plan.transportMode)
);
@@ -38,7 +38,7 @@
contract.hcsFdId || '-'
}}</el-descriptions-item>
<el-descriptions-item label="客商风险">
<template v-if="contract.hcsRiskLevel || contract.hcsBlackStatus">
<template v-if="contract.hcsRiskLevel || contract.kingdeeBlackStatus">
<el-tag
v-if="isBlacklist"
type="danger"
@@ -130,7 +130,7 @@ export default {
name: 'HcsContractPanel',
components: { UploadFilled },
props: {
// 合同详情行(须含 id/hcsSyncStatus/hcsFdId/hcsRiskLevel/hcsBlackStatus/hcsSpecStatus 等)
// 合同详情行(须含 id/hcsSyncStatus/hcsFdId/hcsRiskLevel/kingdeeBlackStatus/hcsSpecStatus 等)
contract: { type: Object, required: true },
},
emits: ['refresh'],
@@ -152,7 +152,7 @@ export default {
return this.contract.hcsSyncMsg || '';
},
isBlacklist() {
return String(this.contract.hcsBlackStatus || '').toLowerCase() === 'yes';
return String(this.contract.kingdeeBlackStatus || '').toLowerCase() === 'yes';
},
specLabel() {
return SPEC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSpecStatus)?.label || '';
@@ -195,7 +195,7 @@
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-form">
<el-row :gutter="16">
<template v-if="isRoad(route)">
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType !== '自运'"><el-select :model-value="route.carrierType !== '自运' ? route.carrierContractId : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType !== '自运' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType !== '自运'"><el-select :model-value="route.carrierType !== '自运' ? carrierSelectedValue(route) : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType !== '自运' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
<el-col :span="6"
><el-form-item label="司机" :required="route.carrierType !== '承运商'"
><el-autocomplete
@@ -255,7 +255,7 @@
></el-col>
</template>
<template v-else>
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType !== '自运'"><el-select :model-value="route.carrierType !== '自运' ? route.carrierContractId : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType !== '自运' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType !== '自运'"><el-select :model-value="route.carrierType !== '自运' ? carrierSelectedValue(route) : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType !== '自运' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="船班号/船次号/班次号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="船长"><el-input v-model="route.captainName" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="联系电话"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
@@ -920,6 +920,8 @@ export default {
route.carrierName = '';
route.carrierId = '';
route.carrierContractId = '';
route.carrierPartyId = '';
route.carrierPartyRole = '';
this.clearRouteDriverVehicleFields(route);
if (value === '承运商') {
route.trailerVehicleNo = '';
@@ -930,15 +932,18 @@ export default {
if (value === '自运') this.applySelfOperatedCarrierDefaults(route);
});
},
handleCarrierContractChange(route, contractId) {
handleCarrierContractChange(route, selectedValue) {
const contract = this.carrierOptions.find(
item =>
this.carrierOptionValue(item) === String(selectedValue || '') ||
[item.contractId, item.carrierName, item.id].some(
value => String(value || '') === String(contractId || '')
value => String(value || '') === String(selectedValue || '')
)
);
route.carrierContractId = route.carrierType !== '自运' ? contract?.contractId || '' : '';
route.carrierId = route.carrierType !== '自运' ? contract?.carrierId || '' : '';
route.carrierPartyId = route.carrierType !== '自运' ? contract?.carrierPartyId || '' : '';
route.carrierPartyRole = route.carrierType !== '自运' ? contract?.carrierPartyRole || '' : '';
route.carrierId = route.carrierType !== '自运' ? contract?.carrierId || contract?.customerId || '' : '';
route.carrierName = contract?.carrierName || '';
this.clearRouteDriverVehicleFields(route);
},
@@ -954,15 +959,23 @@ export default {
this.driverOptions = [];
},
carrierOptionValue(item = {}) {
return item.contractId || item.id || item.value || item.carrierName || '';
return [item.contractId || item.id || '', item.carrierPartyId || '', item.carrierName || ''].join(
'__'
);
},
carrierSelectedValue(route = {}) {
return this.carrierOptionValue({
contractId: route.carrierContractId,
carrierPartyId: route.carrierPartyId,
carrierName: route.carrierName,
});
},
carrierOptionLabel(item) {
const duplicateCount = this.carrierOptions.filter(option => option.carrierName === item.carrierName).length;
if (duplicateCount <= 1) {
return item.carrierName;
}
const roleLabel = item.carrierPartyRole === 'C' ? '丙方' : item.carrierPartyRole === 'B' ? '乙方' : '';
const nameWithRole =
roleLabel && item.carrierName ? `${item.carrierName}(${roleLabel})` : item.carrierName;
const contractLabel = formatContractOptionLabel(item);
return contractLabel ? `${item.carrierName}(${contractLabel})` : item.carrierName;
return contractLabel ? `${nameWithRole} - ${contractLabel}` : nameWithRole;
},
driverOptionLabel(item) { return item.driverName || item.name || ''; },
driverOptionKey(item) { return item.id || item.driverId || this.driverOptionLabel(item) || item.mobile; },
@@ -983,19 +996,22 @@ export default {
...item,
contractId: item.contractId || item.carrierContractId || item.id || '',
carrierName:
item.carrierName ||
item.partyB ||
item.partyBName ||
item.contractPartyB ||
item.customerContractPartyB ||
item.carrierName ||
'',
carrierId:
item.customerId ||
item.partyBId ||
item.partyBUserId ||
item.contractPartyBId ||
item.customerContractPartyBId ||
item.carrierId ||
'',
carrierPartyId: item.carrierPartyId || '',
carrierPartyRole: item.carrierPartyRole || '',
}))
.filter(item => item.carrierName);
} finally {
@@ -1137,7 +1153,7 @@ export default {
id: `${route.segmentNo}-${item.cargoName}-${Date.now()}-${Math.random()}`,
batchNo,
segmentNo: route.segmentNo, relationNo: route.segmentNo, documentType: route.documentType, transportType: route.transportType, carrierType: route.carrierType,
carrierContractId: route.carrierContractId, carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, captainName: route.captainName, cabinNo: route.cabinNo, containerNo: route.containerNo, trailerVehicleNo: route.trailerVehicleNo, escortName: route.escortName, escortPhone: route.escortPhone, mileage: this.normalizeMileage(route.mileage), unitPrice: this.freightItemsForGoods(route, item).unitPrice || '', priceUnit: this.freightItemsForGoods(route, item).priceUnit || '', currency: route.currency || 'RMB', freightAmount: this.freightAmount(this.freightItemsForGoods(route, item)), freightTotal: this.freightTotal(route), otherFeeTotal: route.otherFeeTotal || '', freightJson: this.buildFreightJson(route), routeRemark: route.remark,
carrierContractId: route.carrierContractId, carrierPartyId: route.carrierPartyId, carrierPartyRole: route.carrierPartyRole, carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, captainName: route.captainName, cabinNo: route.cabinNo, containerNo: route.containerNo, trailerVehicleNo: route.trailerVehicleNo, escortName: route.escortName, escortPhone: route.escortPhone, mileage: this.normalizeMileage(route.mileage), unitPrice: this.freightItemsForGoods(route, item).unitPrice || '', priceUnit: this.freightItemsForGoods(route, item).priceUnit || '', currency: route.currency || 'RMB', freightAmount: this.freightAmount(this.freightItemsForGoods(route, item)), freightTotal: this.freightTotal(route), otherFeeTotal: route.otherFeeTotal || '', freightJson: this.buildFreightJson(route), routeRemark: route.remark,
departureName: route.departureName, departureAddress: route.departureAddress, departureContact: route.departureContact, departurePhone: route.departurePhone,
arrivalName: route.arrivalName, arrivalAddress: route.arrivalAddress, arrivalContact: route.arrivalContact, arrivalPhone: route.arrivalPhone,
estimatedStartTime: route.estimatedStartTime, estimatedEndTime: route.estimatedEndTime,
@@ -4650,9 +4650,14 @@ export default {
},
taskCarrierValue: {
get() {
return this.form.carrierType === '自运'
? this.form.carrierName || ''
: this.form.carrierContractId || this.form.carrierName || '';
if (this.form.carrierType === '自运') {
return this.form.carrierName || '';
}
return this.taskCarrierOptionValue({
carrierContractId: this.form.carrierContractId,
carrierPartyId: this.form.carrierPartyId,
carrierName: this.form.carrierName,
});
},
set(value) {
this.handleTaskCarrierChange(value);
@@ -7290,6 +7295,8 @@ export default {
this.form.carrierName = '';
this.form.carrierId = '';
this.form.carrierContractId = '';
this.form.carrierPartyId = '';
this.form.carrierPartyRole = '';
this.hasProjectProcessConfig = false;
this.waybillProcessConfigRows = [];
return;
@@ -7304,6 +7311,8 @@ export default {
this.form.carrierName = '';
this.form.carrierId = '';
this.form.carrierContractId = '';
this.form.carrierPartyId = '';
this.form.carrierPartyRole = '';
this.loadProjectProcessConfigState();
this.loadTaskCarrierOptions(project);
if (this.contractSelectEnabled) {
@@ -7760,6 +7769,8 @@ export default {
this.form.carrierName = '';
this.form.carrierId = '';
this.form.carrierContractId = '';
this.form.carrierPartyId = '';
this.form.carrierPartyRole = '';
this.form.driverId = '';
this.form.driverName = '';
this.form.driverPhone = '';
@@ -7814,7 +7825,9 @@ export default {
},
taskCarrierOptionValue(item = {}) {
if (this.form.carrierType !== '自运') {
return item.carrierContractId || item.value || item.id || '';
if (item.value) return item.value;
return [item.carrierContractId || item.id || '', item.carrierPartyId || '', item.carrierName || '']
.join('__');
}
return (
item.carrierName ||
@@ -7852,55 +7865,76 @@ export default {
names.forEach(append);
return options;
},
parseContractPartyRows(partyJson, fallbackName = '', partyRole = 'B') {
const rows = this.parseJsonArray(partyJson)
.map(item => ({
id: item.id || '',
customerId: item.customerId || '',
customerName: String(item.customerName || '').trim(),
creditCode: item.creditCode || '',
partyRole,
}))
.filter(item => item.customerName);
if (rows.length) return rows;
return String(fallbackName || '')
.split(/[、,,]/)
.map(name => name.trim())
.filter(Boolean)
.map(name => ({
id: '',
customerId: '',
customerName: name,
creditCode: '',
partyRole,
}));
},
getWaybillCarrierContractOptions(contracts = []) {
const carrierNameCounts = contracts.reduce((counts, contract) => {
const carrierName = String(
contract.partyB ||
contract.partyBName ||
contract.contractPartyB ||
contract.customerContractPartyB ||
contract.secondParty ||
contract.secondPartyName ||
''
).trim();
if (carrierName) counts[carrierName] = (counts[carrierName] || 0) + 1;
return counts;
}, {});
return contracts
.map(contract => {
const carrierName = String(
const options = [];
contracts.forEach(contract => {
if (!contract?.id) return;
const parties = [
...this.parseContractPartyRows(contract.partyBJson, contract.partyB, 'B'),
...this.parseContractPartyRows(contract.partyCJson, contract.partyC, 'C'),
];
if (!parties.length) {
const fallbackName = String(
contract.partyB ||
contract.partyBName ||
contract.contractPartyB ||
contract.customerContractPartyB ||
contract.secondParty ||
contract.secondPartyName ||
''
contract.partyBName ||
contract.contractPartyB ||
contract.customerContractPartyB ||
contract.secondParty ||
contract.secondPartyName ||
''
).trim();
if (!carrierName || !contract.id) return null;
const contractIdentifier = contract.contractName || contract.contractNo || contract.id;
return {
if (fallbackName) {
parties.push({
id: '',
customerId: '',
customerName: fallbackName,
creditCode: '',
partyRole: 'B',
});
}
}
const contractIdentifier = contract.contractName || contract.contractNo || contract.id;
parties.forEach(party => {
const roleLabel = party.partyRole === 'C' ? '丙方' : '乙方';
const optionValue = [contract.id, party.id || '', party.customerName].join('__');
options.push({
id: contract.id,
carrierId:
contract.partyBId ||
contract.partyBUserId ||
contract.contractPartyBId ||
contract.customerContractPartyBId ||
contract.secondPartyId ||
contract.secondPartyUserId ||
contract.carrierId ||
'',
value: contract.id,
label:
carrierNameCounts[carrierName] > 1
? `${carrierName}(${contractIdentifier})`
: carrierName,
carrierName,
fullName: carrierName,
carrierId: party.customerId || '',
value: optionValue,
label: `${party.customerName}(${roleLabel}) - ${contractIdentifier}`,
carrierName: party.customerName,
fullName: party.customerName,
carrierContractId: contract.id,
};
})
.filter(Boolean);
carrierPartyId: party.id || '',
carrierPartyRole: party.partyRole,
creditCode: party.creditCode || '',
});
});
});
return options;
},
resetWaybillCarrierContractState() {
this.waybillCarrierContractRequestId += 1;
@@ -7930,6 +7964,8 @@ export default {
this.form.carrierName = '';
this.form.carrierId = '';
this.form.carrierContractId = '';
this.form.carrierPartyId = '';
this.form.carrierPartyRole = '';
}
return options;
})
@@ -8020,10 +8056,14 @@ export default {
);
if (this.form.carrierType !== '自运') {
this.form.carrierContractId = carrier?.carrierContractId || '';
this.form.carrierPartyId = carrier?.carrierPartyId || '';
this.form.carrierPartyRole = carrier?.carrierPartyRole || '';
this.form.carrierId = carrier?.carrierId || '';
this.form.carrierName = carrier?.carrierName || '';
} else {
this.form.carrierId = carrier?.carrierContractId ? '' : carrier?.id || '';
this.form.carrierPartyId = '';
this.form.carrierPartyRole = '';
this.form.carrierName = value || '';
}
// 承运商变更后清空司机/车辆,避免跨组织脏数据