调整基础数据、车船务、项目、合同

This commit is contained in:
2026-09-03 22:40:11 +08:00
parent 024e77801e
commit c63c7c3e33
43 changed files with 821 additions and 239 deletions
@@ -52,7 +52,7 @@
:value="item.value" /></el-select></el-form-item
></el-col>
<el-col :span="8"
><el-form-item label="默认方案"
><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"
@@ -103,12 +103,28 @@
filterable
:disabled="!row.feeType"
:loading="feeItemLoadingMap[feeTypeKey(row)]"
@change="value => handleFeeItemChange(row, value)"
><el-option
v-for="item in feeItemOptions(row)"
:key="item.id || item.name || item.englishName"
:label="item.name || item.englishName"
:value="item.name || item.englishName" /></el-select></template
></el-table-column>
<el-table-column label="税率" width="180"
><template #default="{ row }"
><span v-if="readonly">{{ formatTaxRate(row.taxRate) }}</span
><el-input
v-else
v-model="row.taxRate"
inputmode="decimal"
maxlength="6"
clearable
placeholder="请输入"
@input="value => taxRateInput(row, value)"
><template #append>%</template></el-input
></template
></el-table-column
>
<el-table-column label="计费要素" width="170"
><template #default="{ row }"
><span v-if="readonly">{{ displayValue(row.billingElement) }}</span
@@ -372,6 +388,7 @@ const clone = value => JSON.parse(JSON.stringify(value));
const defaultRule = () => ({
feeType: '',
feeItem: '',
taxRate: '',
billingElement: '',
billingType: '',
billingUnit: '',
@@ -467,7 +484,6 @@ export default {
label: 'cargoName',
value: 'id',
children: 'children',
disabled: (data, node) => node.level === 1,
leaf: 'leaf',
checkStrictly: true,
emitPath: true,
@@ -622,27 +638,64 @@ export default {
feeItemOptions(row) {
return this.feeItems[this.feeTypeKey(row)] || [];
},
formatTaxRate(value) {
return value === undefined || value === null || value === '' ? '-' : `${value}%`;
},
loadRuleItems() {
(this.draft.rules || []).forEach(row => this.loadFeeItems(row.feeType));
},
loadFeeItems(value) {
const key = this.feeTypeKey({ feeType: value });
if (!key || this.feeItems[key]) return;
if (!key) return;
if (this.feeItems[key]) {
this.fillLoadedRuleTaxRates(key);
return;
}
this.feeItemLoadingMap[key] = true;
getFeeItemList(1, 9999, { feeCategory: key })
.then(res => {
const data = res?.data?.data || res?.data || {};
this.feeItems[key] = Array.isArray(data) ? data : data.records || [];
this.fillLoadedRuleTaxRates(key);
})
.finally(() => {
this.feeItemLoadingMap[key] = false;
});
},
fillLoadedRuleTaxRates(key) {
(this.draft.rules || [])
.filter(row => this.feeTypeKey(row) === key)
.forEach(row => this.fillRuleTaxRate(row));
},
handleFeeTypeChange(row, value) {
row.feeType = this.feeTypeKey({ feeType: value });
row.feeItem = '';
row.taxRate = '';
this.loadFeeItems(row.feeType);
},
handleFeeItemChange(row, value) {
row.feeItem = value;
const feeItem = this.feeItemOptions(row).find(
item => String(item.name || item.englishName || '') === String(value || '')
);
row.taxRate = this.hasTaxRate(feeItem?.taxRate) ? String(feeItem.taxRate) : '';
},
fillRuleTaxRate(row) {
if (this.hasTaxRate(row.taxRate) || !row.feeItem) return;
const feeItem = this.feeItemOptions(row).find(
item => String(item.name || item.englishName || '') === String(row.feeItem)
);
if (this.hasTaxRate(feeItem?.taxRate)) row.taxRate = String(feeItem.taxRate);
},
hasTaxRate(value) {
return value !== undefined && value !== null && String(value).trim() !== '';
},
taxRateInput(row, value) {
const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.');
row.taxRate =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
},
billingTypes(row) {
return this.typeMap[row.billingElement] || [];
},
@@ -771,6 +824,7 @@ export default {
const required = [
['feeType', '费用类型'],
['feeItem', '费用项'],
['taxRate', '税率'],
['billingElement', '计费要素'],
['billingType', '计费类型'],
['billingUnit', '计费单位'],
@@ -789,6 +843,13 @@ export default {
return false;
}
feeItemSet.add(feeItem);
if (this.hasTaxRate(row.taxRate)) {
const taxRate = Number(row.taxRate);
if (!/^\d+(\.\d{1,2})?$/.test(String(row.taxRate)) || taxRate < 0 || taxRate > 100) {
this.$message.warning(`${i + 1}行税率必须在0到100之间且最多保留2位小数`);
return false;
}
}
if (
!this.usesRangeUnitPrice(row) &&
(row.unitPrice === undefined ||
@@ -803,8 +864,9 @@ export default {
return false;
}
if (!this.canEditLimit(row)) continue;
const key = row.billingElement;
groups[key] = groups[key] || [];
const billingElement = String(row.billingElement).trim();
const key = JSON.stringify([feeItem, billingElement]);
groups[key] = groups[key] || { feeItem, billingElement, ranges: [] };
for (const range of this.getRanges(row)) {
const lower = Number(range.lowerLimit);
const upper = Number(range.upperLimit);
@@ -830,22 +892,24 @@ export default {
this.$message.warning('计费要素下限不能大于上限');
return false;
}
groups[key].push({ lower, upper });
groups[key].ranges.push({ lower, upper });
}
}
return this.validateRangeGroups(groups);
},
validateRangeGroups(groups) {
const precision = 0.000001;
for (const [element, ranges] of Object.entries(groups)) {
for (const { feeItem, billingElement, ranges } of Object.values(groups)) {
const sorted = [...ranges].sort((a, b) => a.lower - b.lower || a.upper - b.upper);
for (let i = 1; i < sorted.length; i += 1) {
if (sorted[i].lower < sorted[i - 1].upper - precision) {
this.$message.warning(`${element}的计费要素区间不能重叠`);
this.$message.warning(`费用项“${feeItem}${billingElement}的计费要素区间不能重叠`);
return false;
}
if (sorted[i].lower > sorted[i - 1].upper + precision) {
this.$message.warning(`${element}的计费要素区间必须连续,不能存在间隙`);
this.$message.warning(
`费用项“${feeItem}${billingElement}的计费要素区间必须连续,不能存在间隙`
);
return false;
}
}
@@ -1078,25 +1142,27 @@ export default {
},
matchCargoChange(value) {
const path =
Array.isArray(value) && value.length >= 2
Array.isArray(value) && value.length
? value.slice(0, 2).map(item => String(item))
: [];
const item = this.cargoFlatOptions.find(
option => String(option.id) === String(path[1]) && option.path?.length >= 2
option =>
option.path?.length === path.length &&
option.path.every((itemValue, index) => String(itemValue) === path[index])
);
this.matchForm.cargoTypePath = path;
this.matchForm.cargoType = item ? this.cargoLabels(path).join('/') : '';
this.matchForm.cargoTypeCode = item ? item.cargoCode || item.code || item.id : '';
},
resolveCargoPath(condition) {
if (Array.isArray(condition.cargoTypePath) && condition.cargoTypePath.length >= 2)
return condition.cargoTypePath;
if (Array.isArray(condition.cargoTypePath) && condition.cargoTypePath.length)
return condition.cargoTypePath.slice(0, 2).map(item => String(item));
const item = this.cargoFlatOptions.find(
option =>
option.path?.length >= 2 &&
(String(option.cargoCode || option.code || option.id) ===
String(condition.cargoTypeCode) ||
option.cargoName === condition.cargoType)
option.cargoName === condition.cargoType ||
this.cargoLabels(option.path).join('/') === condition.cargoType)
);
return item?.path || [];
},