🔀 合并主干 master 到 dev

This commit is contained in:
2026-10-26 14:18:43 +08:00
10 changed files with 432 additions and 143 deletions
+7 -2
View File
@@ -24,11 +24,16 @@ export const getDetail = id =>
},
});
export const getCarrierContracts = projectIds =>
export const getCarrierContracts = (projectIds, waybillIds = []) =>
request({
url: `${baseUrl}/carrier-contracts`,
method: 'get',
params: { projectIds: (projectIds || []).join(',') },
params: {
projectIds: (projectIds || []).join(','),
...(waybillIds || []).filter(Boolean).length
? { waybillIds: (waybillIds || []).filter(Boolean).join(',') }
: {},
},
});
export const saveDraft = row =>
@@ -48,6 +48,7 @@ export const option = {
searchOrder: 1,
searchslot: true,
slot: true,
overHidden: true,
minWidth: 150,
},
{
@@ -55,6 +56,8 @@ export const option = {
prop: 'useDepartment',
search: true,
searchOrder: 3,
slot: true,
overHidden: true,
minWidth: 140,
},
{
+36 -2
View File
@@ -27,8 +27,14 @@
/>
</el-select>
</el-form-item>
<el-form-item label="合同编号">
<el-input v-model="form.contractNo" disabled />
<el-form-item label="合同编号" prop="contractNo">
<el-input
v-model="form.contractNo"
maxlength="100"
clearable
placeholder="请输入合同编号"
:disabled="isSettlementRuleChange"
/>
</el-form-item>
<el-form-item label="合同名称" prop="contractName">
<el-input
@@ -662,6 +668,24 @@ export default {
'按数量',
],
rules: {
contractNo: [
{
validator: (rule, value, callback) => {
if (this.isSettlementRuleChange) {
return callback();
}
const contractNo = String(value || '').trim();
if (!contractNo) {
return callback(new Error('请输入合同编号'));
}
if (contractNo.length > 100) {
return callback(new Error('合同编号不能超过100个字符'));
}
callback();
},
trigger: 'blur',
},
],
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
contractAmount: [
{ required: true, message: '请输入合同金额', trigger: 'blur' },
@@ -956,6 +980,16 @@ export default {
}
await this.$refs.formRef.validate();
if (!this.isSettlementRuleChange) {
const contractNo = String(this.form.contractNo || '').trim();
if (!contractNo) {
this.$message.warning('请输入合同编号');
return;
}
if (contractNo.length > 100) {
this.$message.warning('合同编号不能超过100个字符');
return;
}
this.form.contractNo = contractNo;
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%');
+51 -4
View File
@@ -901,7 +901,7 @@
<div class="loading-manage-dialog__admission-tip">
<div class="loading-manage-dialog__admission-title">可配载条件</div>
<div class="loading-manage-dialog__admission-text">
同时满足以下条件的运单可被配载:运输方式为「公路」;运单状态为「待执行」或「进行中」;未关联其他有效配载单。
同时满足以下条件的运单可被配载:运输方式为「公路」;运单状态为「待执行」或「进行中」;未关联其他有效配载单;同一配载单的子运单须属同一项目,且承运商合同乙方须存在共同可选值(按各子单客户合同甲方匹配后取交集)。
</div>
<div class="loading-manage-dialog__admission-title">配载后状态说明</div>
<div class="loading-manage-dialog__admission-text">
@@ -1193,6 +1193,16 @@
min-width="160"
show-overflow-tooltip
/>
<el-table-column
prop="carrierContractName"
label="承运商合同"
min-width="160"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.carrierContractName || '-' }}
</template>
</el-table-column>
<el-table-column
prop="planName"
label="计划名称"
@@ -3896,6 +3906,7 @@ export default {
async loadCandidateWaybills() {
this.candidateLoading = true;
try {
const selectedProjectId = this.selectedWaybillRows.map(row => row.projectId).find(Boolean);
const query = {
...this.buildQueryParams(this.candidateQuery),
// 公路、待执行、未配载的判定都在后端可配载开关内
@@ -3905,6 +3916,10 @@ export default {
// 其余账户限定本组织(不展开下级)
allDept: 0,
};
// 已选运单后,候选列表优先限定同一项目,便于再按承运商合同乙方交集筛选
if (selectedProjectId && !query.projectId && !query.projectName) {
query.projectId = selectedProjectId;
}
const res = await getWaybillList(
this.candidatePage.currentPage,
this.candidatePage.pageSize,
@@ -3966,7 +3981,10 @@ export default {
];
if (projectIds.length) {
try {
const response = await loadingApi.getCarrierContracts(projectIds);
const waybillIds = [...this.selectedWaybillRows, ...selectedRows]
.map(row => row.id)
.filter(Boolean);
const response = await loadingApi.getCarrierContracts(projectIds, waybillIds);
const contracts = response?.data?.data || response?.data || [];
const stillAllowed = (Array.isArray(contracts) ? contracts : []).some(
item => String(item.id) === String(contractId)
@@ -4015,6 +4033,19 @@ export default {
exists.add(key);
}
});
// 承运商类型:新增后须仍存在共同的承运商合同乙方(按客户合同甲方匹配后取交集)
if (
String(this.dialogForm.carrierType || '').trim() === '承运商' &&
nextRows.length > this.selectedWaybillRows.length
) {
const compatible = await this.hasCompatibleCarrierContracts(nextRows);
if (!compatible) {
this.$message.warning(
'所选运单在同一项目下没有共同的承运商合同乙方,不能加入同一配载单'
);
return;
}
}
const prevRouteNodes = [...this.routeNodes];
this.selectedWaybillRows = nextRows;
if (this.isRunningEdit) {
@@ -4028,6 +4059,18 @@ export default {
this.candidateAdding = false;
}
},
async hasCompatibleCarrierContracts(rows = []) {
const projectId = (rows || []).map(row => row.projectId).find(Boolean);
const waybillIds = (rows || []).map(row => row.id).filter(Boolean);
if (!projectId || waybillIds.length < 2) return true;
try {
const response = await loadingApi.getCarrierContracts([projectId], waybillIds);
const contracts = response?.data?.data || response?.data || [];
return (Array.isArray(contracts) ? contracts : []).length > 0;
} catch (error) {
return true;
}
},
async addSelectedWaybills() {
await this.addWaybillRows(this.candidateSelection);
},
@@ -4711,9 +4754,13 @@ export default {
}
this.taskCarrierLoading = true;
try {
// 配载单只有一个项目,承运商合同也必须属于该项目
// 配载单只有一个项目;传入子运单后按客户合同甲方匹配并取承运商合同乙方交集
const projectId = this.selectedWaybillRows.map(row => row.projectId).find(Boolean);
const response = await loadingApi.getCarrierContracts(projectId ? [projectId] : []);
const waybillIds = this.selectedWaybillRows.map(row => row.id).filter(Boolean);
const response = await loadingApi.getCarrierContracts(
projectId ? [projectId] : [],
waybillIds
);
if (requestId !== this.taskCarrierRequestId) return [];
const contracts = response.data?.data || response.data || [];
if (this.dialogForm.carrierType !== '自运') {
+35 -15
View File
@@ -251,10 +251,10 @@
/>
</template>
</el-table-column>
<el-table-column label="不含税单价" min-width="130">
<el-table-column label="合计金额" min-width="140">
<template #default="{ row }">
<el-input-number
v-model="row.unitPriceNoTax"
v-model="row.totalAmount"
:disabled="readonly"
:min="0"
:precision="2"
@@ -263,9 +263,6 @@
/>
</template>
</el-table-column>
<el-table-column label="不含税金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.amountNoTax) }}</template>
</el-table-column>
<el-table-column label="税率" min-width="110">
<template #default="{ row }">
<el-select
@@ -282,11 +279,14 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="税额" min-width="120">
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
<el-table-column label="不含税单价" min-width="140" align="right">
<template #default="{ row }">{{ formatUnitPrice(row.unitPriceNoTax) }}</template>
</el-table-column>
<el-table-column label="合计" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.totalAmount) }}</template>
<el-table-column label="不含税金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.amountNoTax) }}</template>
</el-table-column>
<el-table-column label="税额" min-width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
</el-table-column>
<el-table-column label="备注" min-width="170">
<template #default="{ row }">
@@ -1108,15 +1108,30 @@ export default {
addLine(sheet) {
sheet.lines.push(emptyLine());
},
/**
* 填写合计金额 + 税率后反算:
* 合计金额 = 数量 × 不含税单价 × (1 + 税率)
* 不含税单价 = 合计金额 / 数量 / (1 + 税率),保留 6 位小数
* 不含税金额 = 数量 × 不含税单价,保留 2 位小数
*/
recalculateLine(row) {
const quantity = Number(row.quantity || 0);
const unitPrice = Number(row.unitPriceNoTax || 0);
const amountNoTax = Number((quantity * unitPrice).toFixed(2));
const totalAmount = Number(Number(row.totalAmount || 0).toFixed(2));
const rate = Number(row.taxRate || 0) / 100;
row.totalAmount = totalAmount;
row.amountWithTax = totalAmount;
if (quantity <= 0 || totalAmount <= 0) {
row.unitPriceNoTax = 0;
row.amountNoTax = 0;
row.taxAmount = 0;
return;
}
const factor = 1 + rate;
const unitPriceNoTax = Number((totalAmount / quantity / factor).toFixed(6));
const amountNoTax = Number((quantity * unitPriceNoTax).toFixed(2));
row.unitPriceNoTax = unitPriceNoTax;
row.amountNoTax = amountNoTax;
row.taxAmount = rate ? Number((amountNoTax * rate).toFixed(2)) : 0;
row.totalAmount = Number((amountNoTax + row.taxAmount).toFixed(2));
row.amountWithTax = row.totalAmount;
row.taxAmount = Number((totalAmount - amountNoTax).toFixed(2));
},
validateBusiness() {
if (!this.selectedDetailIds.length) throw new Error('请至少选择一条开票明细');
@@ -1129,12 +1144,14 @@ export default {
for (const line of sheet.lines) {
if (!line.goodsCategory || !line.goodsName) throw new Error('请完整填写商品和服务信息');
if (
[line.quantity, line.unitPriceNoTax, line.amountNoTax, line.taxRate].some(
[line.quantity, line.totalAmount, line.unitPriceNoTax, line.amountNoTax, line.taxRate].some(
Number.isNaN
)
) {
throw new Error('开票商品行金额格式不正确');
}
if (Number(line.quantity || 0) <= 0) throw new Error('开票商品行数量必须大于0');
if (Number(line.totalAmount || 0) <= 0) throw new Error('开票商品行合计金额必须大于0');
}
}
},
@@ -1234,6 +1251,9 @@ export default {
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
formatUnitPrice(value) {
return Number(value || 0).toFixed(6);
},
},
};
</script>
@@ -1388,16 +1388,12 @@ export default {
},
immediate: true,
},
summaryTotal(value) {
this.form.settlementAmount = Number(value || 0).toFixed(2);
this.form.localSettlementAmount = (
Number(value || 0) * Number(this.form.exchangeRate || 1)
).toFixed(2);
summaryTotal() {
this.syncSettlementAmountsFromSummary();
},
'form.exchangeRate'(value) {
this.form.localSettlementAmount = (
Number(this.summaryTotal || 0) * Number(value || 1)
).toFixed(2);
'form.exchangeRate'() {
if (this._skipExchangeRateRebuild) return;
this.buildSummaryFeesFromDetails();
},
},
methods: {
@@ -1683,8 +1679,9 @@ export default {
current.settlementAmount = current.originalAmount;
summaryMap.set(key, current);
});
// 先保留预结算手工费用行,再按明细本位币 + 结算汇率重算系统归集行
this.summaryFees = [...summaryMap.values()];
if (!this.summaryFees.length) await this.refreshSummaryFees();
await this.refreshSummaryFees();
},
async loadAllContracts() {
const response = await getContractOptions('');
@@ -1786,13 +1783,19 @@ export default {
},
async fetchBfiExchangeRate() {
const currency = this.form.currency;
if (!currency || currency === 'RMB') return;
if (!currency || currency === 'RMB') {
this.buildSummaryFeesFromDetails();
return;
}
if (!this.form.exchangeRateDate) return;
try {
const { data } = await getBfiExchangeRate(currency, this.form.exchangeRateDate);
const rateData = data?.data;
if (rateData?.excval != null) {
this._skipExchangeRateRebuild = true;
this.form.exchangeRate = Number(rateData.excval);
this._skipExchangeRateRebuild = false;
this.buildSummaryFeesFromDetails();
} else {
this.$message.warning(`未查询到 ${currency} 的BFI汇率数据`);
}
@@ -1800,6 +1803,37 @@ export default {
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
}
},
/**
* 明细金额按本位币理解;外币结算时结算合计原金额/结算金额 = 本位币金额 / 结算汇率。
*/
convertLocalToSettlementAmount(localAmount) {
const amount = Number(localAmount || 0);
if (!Number.isFinite(amount)) return 0;
if (this.isLocalCurrencySettlement) {
return Number(amount.toFixed(2));
}
const rate = Number(this.form.exchangeRate || 0);
if (rate <= 0) return 0;
return Number((amount / rate).toFixed(2));
},
/**
* 结算金额取结算合计原币汇总值;本位币合计 = 结算金额 × 结算汇率。
*/
syncSettlementAmountsFromSummary() {
const settlementTotal = this.summaryFees.reduce(
(total, row) => total + Number(row.settlementAmount || 0),
0
);
this.form.settlementAmount = Number(settlementTotal || 0).toFixed(2);
if (this.isLocalCurrencySettlement) {
this.form.localSettlementAmount = this.form.settlementAmount;
this.form.exchangeRate = 1;
return;
}
const rate = Number(this.form.exchangeRate || 0);
this.form.localSettlementAmount =
rate > 0 ? (Number(this.form.settlementAmount || 0) * rate).toFixed(2) : '0.00';
},
openCandidateDialog() {
this.candidate.visible = true;
this.candidate.page.current = 1;
@@ -2021,8 +2055,9 @@ export default {
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
);
const append = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => {
const originalAmount = Number(originalValue || 0);
const settlementAmount = Number(settlementValue || 0);
// 明细费用按本位币归集,外币结算时再按汇率换算成结算原币金额
const originalAmount = this.convertLocalToSettlementAmount(originalValue);
const settlementAmount = this.convertLocalToSettlementAmount(settlementValue);
if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
return;
}
@@ -2060,6 +2095,7 @@ export default {
});
const manualRows = this.summaryFees.filter(row => Number(row.manualFlag) === 1);
this.summaryFees = [...generatedMap.values(), ...manualRows];
this.syncSettlementAmountsFromSummary();
},
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少费用快照时退化为明细自身的汇总行。
resolveDetailFeePairs(detail) {
@@ -1332,9 +1332,12 @@ export default {
if (value) this.initialize();
},
},
summaryTotal(value) {
this.form.settlementAmount = Number(value || 0).toFixed(2);
this.recalculateLocalAmount();
summaryTotal() {
this.syncSettlementAmountsFromSummary();
},
'form.exchangeRate'() {
if (this._skipExchangeRateRebuild) return;
this.buildSummaryFeesFromDetails();
},
filteredDetails: {
handler(list) {
@@ -1890,8 +1893,9 @@ export default {
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
);
const appendSummary = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => {
const originalAmount = Number(originalValue || 0);
const settlementAmount = Number(settlementValue || 0);
// 明细费用按本位币归集,外币结算时再按汇率换算成结算原币金额
const originalAmount = this.convertLocalToSettlementAmount(originalValue);
const settlementAmount = this.convertLocalToSettlementAmount(settlementValue);
if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
return;
}
@@ -1946,7 +1950,7 @@ export default {
});
nextFeeMap.forEach(row => orderedFees.push(row));
this.summaryFees = orderedFees;
this.recalculateLocalAmount();
this.syncSettlementAmountsFromSummary();
},
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少来源费用快照时退化为明细自身的汇总行。
resolveDetailFeePairs(detail) {
@@ -2034,17 +2038,21 @@ export default {
},
handleExchangeRateDateChange() {
this.fetchBfiExchangeRate();
this.recalculateLocalAmount();
},
async fetchBfiExchangeRate() {
if (!this.form.currency || this.form.currency === 'RMB') return;
if (!this.form.currency || this.form.currency === 'RMB') {
this.buildSummaryFeesFromDetails();
return;
}
if (!this.form.exchangeRateDate) return;
try {
const { data } = await getBfiExchangeRate(this.form.currency, this.form.exchangeRateDate);
const rateData = data?.data;
if (rateData?.excval != null) {
this._skipExchangeRateRebuild = true;
this.form.exchangeRate = Number(rateData.excval);
this.recalculateLocalAmount();
this._skipExchangeRateRebuild = false;
this.buildSummaryFeesFromDetails();
} else {
this.$message.warning(`未查询到 ${this.form.currency} 的BFI汇率数据`);
}
@@ -2052,9 +2060,39 @@ export default {
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
}
},
/**
* 明细金额按本位币理解;外币结算时结算合计原金额/结算金额 = 本位币金额 / 结算汇率。
*/
convertLocalToSettlementAmount(localAmount) {
const amount = Number(localAmount || 0);
if (!Number.isFinite(amount)) return 0;
if (this.isLocalCurrencySettlement) {
return Number(amount.toFixed(2));
}
const rate = Number(this.form.exchangeRate || 0);
if (rate <= 0) return 0;
return Number((amount / rate).toFixed(2));
},
/**
* 结算金额取结算合计原币汇总值;本位币合计 = 结算金额 × 结算汇率。
*/
syncSettlementAmountsFromSummary() {
const settlementTotal = this.summaryFees.reduce(
(total, row) => total + Number(row.settlementAmount || 0),
0
);
this.form.settlementAmount = Number(settlementTotal || 0).toFixed(2);
if (this.isLocalCurrencySettlement) {
this.form.localSettlementAmount = this.form.settlementAmount;
this.form.exchangeRate = 1;
return;
}
const rate = Number(this.form.exchangeRate || 0);
this.form.localSettlementAmount =
rate > 0 ? (Number(this.form.settlementAmount || 0) * rate).toFixed(2) : '0.00';
},
recalculateLocalAmount() {
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0);
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
this.buildSummaryFeesFromDetails();
},
openCandidateDialog() {
if (!this.form.contractId) {
+150 -35
View File
@@ -59,11 +59,22 @@
</el-button>
</template>
<template #organizationName="{ row }">
<span
v-if="isBlankOrganizationName(row.organizationName)"
class="organization-name--empty"
>未分配组织</span>
<span v-else>{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}</span>
<el-tooltip
:content="formatOrgDisplayName(row.organizationName)"
placement="top"
:disabled="!formatOrgDisplayName(row.organizationName)"
>
<span class="org-cell-text">{{ formatOrgDisplayName(row.organizationName) }}</span>
</el-tooltip>
</template>
<template #useDepartment="{ row }">
<el-tooltip
:content="formatOrgDisplayName(row.useDepartment)"
placement="top"
:disabled="!formatOrgDisplayName(row.useDepartment)"
>
<span class="org-cell-text">{{ formatOrgDisplayName(row.useDepartment) }}</span>
</el-tooltip>
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
@@ -319,26 +330,58 @@
</el-col>
<el-col :span="6">
<el-form-item label="所有组织" prop="organizationName">
<el-cascader
v-model="organizationCascaderValue"
:options="organizationTreeOptions"
:props="organizationCascaderProps"
filterable
clearable
placeholder="请选择"
/>
<el-tooltip
:content="
formatOrgCascaderPath(organizationCascaderPath) ||
formatOrgDisplayName(vehicleForm.organizationName)
"
placement="top"
:disabled="
!(
formatOrgCascaderPath(organizationCascaderPath) ||
formatOrgDisplayName(vehicleForm.organizationName)
)
"
>
<div class="org-cascader-tooltip">
<el-cascader
v-model="organizationCascaderValue"
:options="organizationFormTreeOptions"
:props="formOrgCascaderProps"
filterable
clearable
placeholder="请选择"
/>
</div>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="使用部门" prop="useDepartment">
<el-cascader
v-model="useDepartmentCascaderValue"
:options="organizationTreeOptions"
:props="organizationCascaderProps"
filterable
clearable
placeholder="请选择"
/>
<el-tooltip
:content="
formatOrgCascaderPath(useDepartmentCascaderPath) ||
formatOrgDisplayName(vehicleForm.useDepartment)
"
placement="top"
:disabled="
!(
formatOrgCascaderPath(useDepartmentCascaderPath) ||
formatOrgDisplayName(vehicleForm.useDepartment)
)
"
>
<div class="org-cascader-tooltip">
<el-cascader
v-model="useDepartmentCascaderValue"
:options="useDepartmentFormTreeOptions"
:props="formOrgCascaderProps"
filterable
clearable
placeholder="请选择"
/>
</div>
</el-tooltip>
</el-form-item>
</el-col>
</el-row>
@@ -656,8 +699,17 @@ export default {
vehicleCertificateUploads: {},
uploadHeaders: getUploadHeaders(),
organizationOptions: [],
// 搜索栏组织级联追加「未分配组织」一项(哨兵值见 organization-search mixin)
enableUnassignedOrgFilter: true,
// 表单两个级联各自维护路径,避免名称回写互相覆盖;不要与搜索栏共用同一棵树节点
organizationCascaderPath: [],
useDepartmentCascaderPath: [],
formOrgCascaderProps: {
value: 'id',
label: 'label',
children: 'children',
emitPath: true,
checkStrictly: true,
expandTrigger: 'hover',
},
vehicleTypeOptions: [
{ label: '高栏车', value: '高栏' },
{ label: '低栏车', value: '低栏' },
@@ -669,7 +721,7 @@ export default {
businessRelationOptions: ['自有', '管理挂靠', '业务合作'],
energyTypeOptions: ['柴油', '汽油', '新能源', '天然气'],
baseRules: {
// 所属组织可为空:为空表示未分配组织,数据权限为空集
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
useDepartment: [{ required: true, message: '请选择使用部门', trigger: 'change' }],
plateNo: [
{ required: true, message: '请输入车牌号', trigger: 'change' },
@@ -703,22 +755,31 @@ export default {
: [{ required: true, message: '请选择行驶证有效期', trigger: 'change' }],
};
},
// 所属组织级联:表单存组织名称,级联控件用 id 路径,两者在此双向转换
// 两个级联必须使用克隆后的独立树,否则 checkStrictly 会在节点上互相抢选中态
organizationFormTreeOptions() {
return this.cloneOrgTree(this.organizationTreeOptions);
},
useDepartmentFormTreeOptions() {
return this.cloneOrgTree(this.organizationTreeOptions);
},
// 所属组织级联:本地保存 id 路径,名称同步到表单字段供后端使用
organizationCascaderValue: {
get() {
return this.resolveOrgPath(this.vehicleForm.organizationName);
return this.organizationCascaderPath;
},
set(path) {
this.vehicleForm.organizationName = this.resolveOrgName(path);
this.organizationCascaderPath = Array.isArray(path) ? [...path] : [];
this.vehicleForm.organizationName = this.resolveOrgName(this.organizationCascaderPath);
},
},
// 使用部门级联:与所属组织同一套部门树
// 使用部门级联:独立路径状态,避免与所属组织共用计算回写导致选不中
useDepartmentCascaderValue: {
get() {
return this.resolveOrgPath(this.vehicleForm.useDepartment);
return this.useDepartmentCascaderPath;
},
set(path) {
this.vehicleForm.useDepartment = this.resolveOrgName(path);
this.useDepartmentCascaderPath = Array.isArray(path) ? [...path] : [];
this.vehicleForm.useDepartment = this.resolveOrgName(this.useDepartmentCascaderPath);
},
},
isAdmin() {
@@ -751,10 +812,49 @@ export default {
this.initDeptTree();
this.loadOrganizationOptions();
},
watch: {
organizationTreeOptions() {
if (this.vehicleBox) {
this.syncOrgCascaderPaths();
}
},
},
methods: {
hasPermission(code) {
return this.isAdmin || this.permission?.[code] === true;
},
/** 去掉组织名称前导空白,供列表与提示展示 */
formatOrgDisplayName(name) {
return String(name || '').replace(/^[\s\u3000]+/, '');
},
/** 级联路径还原完整组织层级文案,悬停展示用 */
formatOrgCascaderPath(path) {
const ids = Array.isArray(path)
? path.filter(item => item !== undefined && item !== null && item !== '')
: [];
if (!ids.length) return '';
const labels = [];
let nodes = this.organizationTreeOptions || [];
for (const id of ids) {
const node = nodes.find(item => String(item.id) === String(id));
if (!node) break;
labels.push(node.rawLabel || node.label || '');
nodes = node.children || [];
}
return labels.filter(Boolean).join(' / ');
},
cloneOrgTree(tree = []) {
return (tree || []).map(item => ({
id: String(item.id),
label: item.label,
rawLabel: item.rawLabel,
children: item.children?.length ? this.cloneOrgTree(item.children) : undefined,
}));
},
syncOrgCascaderPaths() {
this.organizationCascaderPath = this.resolveOrgPath(this.vehicleForm.organizationName);
this.useDepartmentCascaderPath = this.resolveOrgPath(this.vehicleForm.useDepartment);
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
this.organizationOptions = this.flattenDeptOptions(res.data.data || []);
@@ -766,6 +866,9 @@ export default {
if (this.vehicleBox && !this.vehicleForm.useDepartment) {
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
}
if (this.vehicleBox) {
this.syncOrgCascaderPaths();
}
});
},
flattenDeptOptions(tree = [], level = 0) {
@@ -860,6 +963,7 @@ export default {
this.vehicleForm = emptyForm();
this.vehicleForm.organizationName = this.getCurrentOrganizationName();
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
this.syncOrgCascaderPaths();
this.syncPlateNo(false);
this.vehicleBox = true;
return;
@@ -873,6 +977,7 @@ export default {
if (!this.vehicleForm.useDepartment) {
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
}
this.syncOrgCascaderPaths();
this.splitPlateNo();
this.syncPlateNo(false);
this.vehicleBox = true;
@@ -884,6 +989,8 @@ export default {
},
resetVehicle() {
this.vehicleForm = emptyForm();
this.organizationCascaderPath = [];
this.useDepartmentCascaderPath = [];
this.vehicleCertificateUploads = {};
this.readonly = false;
this.submitLoading = false;
@@ -1503,11 +1610,6 @@ export default {
}
}
// 空组织记录在列表里的占位文案
.organization-name--empty {
color: #909399;
}
// 编辑弹窗:灰底 + section-card 白卡分组
.vehicle-form {
:deep(.el-form-item__label) {
@@ -1541,7 +1643,20 @@ export default {
}
}
.org-cell-text {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
.vehicle-form {
.org-cascader-tooltip {
width: 100%;
}
:deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper),
:deep(.el-select),
@@ -102,15 +102,10 @@
/>
</el-form-item>
<el-form-item label="减分最高扣分">
<el-input
:model-value="configForm.maxMinusScore"
placeholder="所有减分项合计不得超过该分值"
clearable
style="width: 500px"
@input="handleMaxMinusScoreInput"
>
<el-input :model-value="HARDCODED_MAX_MINUS_SCORE" disabled style="width: 500px">
<template #append>分</template>
</el-input>
<div class="score-config__hint">暂定最高扣 15 分,加分合计同样最高按 15 分计</div>
</el-form-item>
<el-form-item label="备注">
<el-input
@@ -500,9 +495,15 @@ const standardNumberFields = [
'creditLimitIncrease',
];
/** 暂定:加分/减分合计上限均为 15 分,超出按 15 分计 */
const HARDCODED_MAX_PLUS_SCORE = 15;
const HARDCODED_MAX_MINUS_SCORE = 15;
export default {
data() {
return {
HARDCODED_MAX_PLUS_SCORE,
HARDCODED_MAX_MINUS_SCORE,
form: {},
query: {},
loading: true,
@@ -639,7 +640,7 @@ export default {
name: '',
remark: '',
standardDescription: '',
maxMinusScore: '',
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
status: 3,
categories: this.defaultCategories(),
standards: [],
@@ -696,7 +697,8 @@ export default {
});
return {
...detail,
maxMinusScore: this.normalizePlainDecimal(detail.maxMinusScore),
// 暂定写死最高扣分,不读取历史配置值
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
categories: categories.map(category => ({
...(map[category.categoryCode] || category),
categoryName: category.categoryName,
@@ -704,14 +706,6 @@ export default {
standards: (detail.standards || []).map(standard => this.normalizeStandard(standard)),
};
},
handleMaxMinusScoreInput(value) {
const raw = String(value ?? '')
.replace(/[^\d.]/g, '')
.replace(/^\./, '')
.replace(/(\..*)\./g, '$1')
.replace(/^(\d+)(\.\d{0,2})?.*$/, '$1$2');
this.configForm.maxMinusScore = raw;
},
openConfig(row, readonly = false) {
this.readonly = readonly;
if (row && row.id) {
@@ -837,9 +831,7 @@ export default {
},
prepareSubmitConfig(config) {
const payload = JSON.parse(JSON.stringify(config || {}));
const maxMinus = this.normalizePlainDecimal(payload.maxMinusScore);
payload.maxMinusScore =
maxMinus === '' || maxMinus == null ? null : this.toScoreValue(maxMinus);
payload.maxMinusScore = this.toScoreValue(HARDCODED_MAX_MINUS_SCORE);
(payload.categories || []).forEach(category => {
(category.items || []).forEach(item => {
item.score = this.toScoreValue(item.score);
@@ -1096,14 +1088,8 @@ export default {
this.$message.warning('请输入评定表名称');
return false;
}
if (
this.configForm.maxMinusScore !== '' &&
this.configForm.maxMinusScore != null &&
!this.isNonNegativeDecimal(this.configForm.maxMinusScore)
) {
this.$message.warning('减分最高扣分须为不小于0的数字,最多两位小数');
return false;
}
// 减分最高扣分暂定写死为 15,提交前强制回填,避免历史脏数据触发校验
this.configForm.maxMinusScore = HARDCODED_MAX_MINUS_SCORE;
const basic = this.configForm.categories.find(item => item.categoryCode === 'basic');
if (!basic || basic.items.length === 0) {
this.$message.warning('至少配置1个基础得分项目');
@@ -1586,6 +1572,13 @@ export default {
overflow: auto;
}
.score-config__hint {
margin-top: 6px;
color: #909399;
font-size: 12px;
line-height: 1.4;
}
:deep(.score-config__name-input) {
width: 500px;
}
+31 -33
View File
@@ -1588,6 +1588,9 @@ const customerArchiveFormRoute = '/vehicle/customer-archive/form';
const customerArchiveListRefreshKey = 'customer-archive-list-refresh';
/** 客商所属组织固定为顶层组织 */
const FIXED_OWNER_DEPT_NAME = '广西现代物流集团有限公司';
/** 暂定:加分/减分合计上限均为 15 分,超出按 15 分计 */
const HARDCODED_MAX_PLUS_SCORE = 15;
const HARDCODED_MAX_MINUS_SCORE = 15;
const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'],
@@ -2291,16 +2294,18 @@ export default {
{ categoryCode: 'plus', categoryName: '加分项目' },
{ categoryCode: 'minus', categoryName: '减分项目' },
];
const maxMinus = Number(this.currentScore?.maxMinusScore);
const hasMaxMinus = Number.isFinite(maxMinus) && maxMinus >= 0;
const rows = categoryMap.map((category, index) => {
const details = this.getScoreCategoryDetails(category.categoryCode);
let selfScore = details.reduce((sum, item) => sum + Number(item.selfScore || 0), 0);
let reviewScore = details.reduce((sum, item) => sum + Number(item.reviewScore || 0), 0);
// 减分项目合计受量化表「最高扣分」限制
if (category.categoryCode === 'minus' && hasMaxMinus) {
selfScore = Math.min(selfScore, maxMinus);
reviewScore = Math.min(reviewScore, maxMinus);
// 暂定:加分/减分合计最高按 15 分计
if (category.categoryCode === 'plus') {
selfScore = Math.min(selfScore, HARDCODED_MAX_PLUS_SCORE);
reviewScore = Math.min(reviewScore, HARDCODED_MAX_PLUS_SCORE);
}
if (category.categoryCode === 'minus') {
selfScore = Math.min(selfScore, HARDCODED_MAX_MINUS_SCORE);
reviewScore = Math.min(reviewScore, HARDCODED_MAX_MINUS_SCORE);
}
return {
...category,
@@ -4216,7 +4221,8 @@ export default {
scoreDate: this.$dayjs().format('YYYY-MM-DD'),
applyCreditLimit: this.archiveForm.applyCreditLimit || '',
maxCreditLimit: '',
maxMinusScore: '',
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
maxPlusScore: HARDCODED_MAX_PLUS_SCORE,
creditLevel: '',
standards: [],
tempApplyCreditLimit: '',
@@ -4236,10 +4242,8 @@ export default {
: score.applyCreditLimit
),
maxCreditLimit: this.normalizeOptionalAmount(score.maxCreditLimit),
maxMinusScore:
score.maxMinusScore === undefined || score.maxMinusScore === null
? ''
: score.maxMinusScore,
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
maxPlusScore: HARDCODED_MAX_PLUS_SCORE,
tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit),
attachments: score.attachments || this.parseAttachments(score.proofAttachments),
details: (score.details || []).map(detail => ({
@@ -4699,11 +4703,8 @@ export default {
applyCreditLimit: this.currentScore.applyCreditLimit,
maxCreditLimit: '',
creditLevel: '',
maxMinusScore:
quantificationData.maxMinusScore ??
scoreTemplate.maxMinusScore ??
this.currentScore.maxMinusScore ??
'',
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
maxPlusScore: HARDCODED_MAX_PLUS_SCORE,
tempApplyCreditLimit: this.currentScore.tempApplyCreditLimit,
tempCreditStartDate: this.currentScore.tempCreditStartDate,
tempCreditEndDate: this.currentScore.tempCreditEndDate,
@@ -4722,14 +4723,11 @@ export default {
},
applyMaxMinusScoreFromQuantification(quantificationId) {
if (!quantificationId) return Promise.resolve();
return getCreditScoreQuantificationDetail(quantificationId).then(res => {
const data = res?.data?.data || {};
this.currentScore.maxMinusScore =
data.maxMinusScore === undefined || data.maxMinusScore === null
? ''
: data.maxMinusScore;
this.calculateScore(this.currentScore);
});
// 暂定写死加分/减分上限,不再依赖量化表配置值
this.currentScore.maxMinusScore = HARDCODED_MAX_MINUS_SCORE;
this.currentScore.maxPlusScore = HARDCODED_MAX_PLUS_SCORE;
this.calculateScore(this.currentScore);
return Promise.resolve();
},
normalizeScoreTemplateDetails(template = {}) {
const categoryDetails = (template.categories || []).flatMap(category =>
@@ -5073,17 +5071,17 @@ export default {
const plusDetails = this.getScoreCategoryDetailsByList(details, 'plus');
const minusDetails = this.getScoreCategoryDetailsByList(details, 'minus');
const sumScore = (list, prop) => list.reduce((sum, item) => sum + Number(item[prop] || 0), 0);
const rawPlusSelf = sumScore(plusDetails, 'selfScore');
const rawPlusReview = sumScore(plusDetails, 'reviewScore');
const rawMinusSelf = sumScore(minusDetails, 'selfScore');
const rawMinusReview = sumScore(minusDetails, 'reviewScore');
// 量化表「减分最高扣分」:所有减分项合计不得超过该上限
const maxMinus = Number(score.maxMinusScore);
const hasMaxMinus = Number.isFinite(maxMinus) && maxMinus >= 0;
const minusSelf = hasMaxMinus ? Math.min(rawMinusSelf, maxMinus) : rawMinusSelf;
const minusReview = hasMaxMinus ? Math.min(rawMinusReview, maxMinus) : rawMinusReview;
const selfScore =
sumScore(basicDetails, 'selfScore') + sumScore(plusDetails, 'selfScore') - minusSelf;
const reviewScore =
sumScore(basicDetails, 'reviewScore') + sumScore(plusDetails, 'reviewScore') - minusReview;
// 暂定:加分/减分合计最高按 15 分计
const plusSelf = Math.min(rawPlusSelf, HARDCODED_MAX_PLUS_SCORE);
const plusReview = Math.min(rawPlusReview, HARDCODED_MAX_PLUS_SCORE);
const minusSelf = Math.min(rawMinusSelf, HARDCODED_MAX_MINUS_SCORE);
const minusReview = Math.min(rawMinusReview, HARDCODED_MAX_MINUS_SCORE);
const selfScore = sumScore(basicDetails, 'selfScore') + plusSelf - minusSelf;
const reviewScore = sumScore(basicDetails, 'reviewScore') + plusReview - minusReview;
const hasReviewScore = details.some(
item => !this.isBlankScore(item.reviewScore)
);