1、调整业务模块

This commit is contained in:
2026-09-02 03:22:51 +08:00
parent 80a15fc626
commit 252df72f87
19 changed files with 1354 additions and 177 deletions
+1
View File
@@ -7,6 +7,7 @@ const api = createCrudApi(baseUrl);
export const getList = api.getList;
export const getDetail = api.getDetail;
export const submit = api.submit;
export const saveDraft = data => request({ url: `${baseUrl}/save-draft`, method: 'post', data });
export const remove = api.remove;
export const copy = api.copy;
export const cancel = api.cancel;
+6
View File
@@ -12,6 +12,12 @@ export const getAvailableOptions = (keyword, deptId, selectedId) =>
method: 'get',
params: { keyword, deptId, selectedId },
});
export const getAvailablePage = (current, size, keyword, deptId, selectedId) =>
request({
url: `${baseUrl}/available-page`,
method: 'get',
params: { current, size, keyword, deptId, selectedId },
});
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
-5
View File
@@ -24,11 +24,6 @@ export const paymentTypeOptions = [
{ label: '进度预付', value: 'progress_advance' },
{ label: '结算付款', value: 'settlement_payment' },
];
export const paymentMethodOptions = [
{ label: '银行转账', value: 'bank_transfer' },
{ label: '银行承兑汇票', value: 'bank_draft' },
{ label: '商业承兑汇票', value: 'commercial_draft' },
];
export const approvalStatusOptions = [
{ label: '草稿', value: 'draft' },
{ label: '审批中', value: 'reviewing' },
+30 -4
View File
@@ -1,7 +1,6 @@
import {
auditColumns,
createCrudOption,
dataSourceOptions,
phoneRule,
planStatusOptions,
selectRule,
@@ -9,6 +8,12 @@ import {
withSearchPlaceholders,
} from './common';
const transportPlanDataSourceOptions = [
{ label: '批量导入', value: '批量导入' },
{ label: '手工创建', value: '手工创建' },
{ label: '外部系统', value: '外部系统' },
];
const listDicFormatter = res => {
const data = res?.data || res;
if (Array.isArray(data)) return data;
@@ -61,7 +66,17 @@ const getSecondCargoTypeName = item =>
'货物';
const formatGoodsInfo = row => {
const rows = parseGoodsRows(row.goodsJson);
const rows = [
row.goodsJson,
row.goodsList,
row.goodsRows,
row.cargoList,
row.cargoRows,
row.goods,
]
.map(source => parseGoodsRows(source))
.filter(source => source.length)
.sort((left, right) => right.length - left.length)[0] || [];
if (!rows.length) return row.goodsInfo || '';
const groups = rows.reduce((result, item = {}) => {
@@ -78,11 +93,22 @@ const formatGoodsInfo = row => {
item.goodsQuantity,
item.quantityUnit,
item.goodsQuantityUnit,
item.cargoUnit,
item.unit,
].some(Boolean);
if (!hasGoodsInfo) return result;
const unit = item.quantityUnit || item.goodsQuantityUnit || item.unit || '';
const unit = String(
item.quantityUnit ||
item.goodsQuantityUnit ||
item.cargoUnit ||
item.unit ||
row.quantityUnit ||
row.goodsQuantityUnit ||
row.cargoUnit ||
row.unit ||
''
).trim();
if (!result[unit]) {
result[unit] = {
typeName: getSecondCargoTypeName(item),
@@ -388,7 +414,7 @@ export const option = {
type: 'select',
search: true,
searchOrder: 1,
dicData: dataSourceOptions,
dicData: transportPlanDataSourceOptions,
minWidth: 120,
addDisplay: false,
editDisplay: false,
+63 -4
View File
@@ -207,6 +207,26 @@ const getFirstValue = (row, props) => {
const getGoodsRows = row => parseJsonArray(row.goodsList || row.goodsRows || row.goodsJson);
const getDetailedGoodsRows = row => {
const goodsRows = [
row.goodsJson,
row.goodsList,
row.goodsRows,
row.cargoList,
row.cargoRows,
row.goods,
]
.map(source => {
if (source === undefined || source === null || source === '') return [];
if (Array.isArray(source)) return source;
if (typeof source === 'object') return [source];
return parseJsonArray(source);
})
.filter(source => source.length)
.sort((left, right) => right.length - left.length);
return goodsRows[0] || [];
};
const getBillingRows = row => {
const freightRows = parseJsonArray(row.freightList || row.freightRows || row.freightJson);
if (freightRows.length) return freightRows;
@@ -218,9 +238,25 @@ const getBillingRows = row => {
const joinText = list => list.filter(item => !isEmpty(item)).join('/');
const formatGoodsInfo = row => {
const goodsRows = [
row.goodsJson,
row.goodsList,
row.goodsRows,
row.cargoList,
row.cargoRows,
row.goods,
]
.map(source => {
if (source === undefined || source === null || source === '') return [];
if (Array.isArray(source)) return source;
if (typeof source === 'object') return [source];
return parseJsonArray(source);
})
.filter(source => source.length)
.sort((left, right) => right.length - left.length)[0] || [];
const text = getFirstValue(row, ['goodsInfo', 'cargoInfo']);
if (text) return text;
return getGoodsRows(row)
if (!goodsRows.length) return text || '';
return goodsRows
.map(item => {
const name = getFirstValue(item, ['cargoName', 'goodsName', 'name']);
const type = getFirstValue(item, ['cargoType', 'goodsType', 'typeName']);
@@ -228,12 +264,13 @@ const formatGoodsInfo = row => {
normalizeNumericDisplayValue(
getFirstValue(item, ['quantity', 'cargoQuantity', 'goodsQuantity'])
),
getFirstValue(item, ['quantityUnit', 'cargoUnit', 'unit']),
getFirstValue(item, ['quantityUnit', 'goodsQuantityUnit', 'cargoUnit', 'unit']) ||
getFirstValue(row, ['quantityUnit', 'goodsQuantityUnit', 'cargoUnit', 'unit']),
]);
return joinText([name, type, quantity]);
})
.filter(Boolean)
.join('; ');
.join('; ') || text || '';
};
const formatGoodsField = (row, props) => {
@@ -246,6 +283,28 @@ const formatGoodsField = (row, props) => {
const formatUnitPrice = row => {
const value = normalizeNumericDisplayValue(getFirstValue(row, ['unitPrice', 'price']));
const unit = getFirstValue(row, ['priceUnit', 'billingUnit', 'unit']);
const goodsRows = getDetailedGoodsRows(row);
if (goodsRows.length > 1) {
const billingRows = getBillingRows(row);
const goodsPrices = goodsRows
.map((item, index) => {
const itemPrice = normalizeNumericDisplayValue(
getFirstValue(item, ['unitPrice', 'price'])
);
if (!isEmpty(itemPrice)) return itemPrice;
return normalizeNumericDisplayValue(
getFirstValue(billingRows[index] || {}, ['unitPrice', 'price'])
);
})
.filter(itemPrice => !isEmpty(itemPrice));
const priceKeys = goodsPrices.map(itemPrice => {
const numericPrice = Number(itemPrice);
return Number.isFinite(numericPrice) ? String(numericPrice) : String(itemPrice).trim();
});
if (new Set(priceKeys).size > 1) return '-';
}
if (!isEmpty(value)) return unit ? `${value}(${unit})` : value;
const billing = getBillingRows(row).find(
item => !isEmpty(normalizeNumericDisplayValue(item.unitPrice))
+2 -2
View File
@@ -21,8 +21,8 @@ export const invoiceApplicationDetailColumns = [
{ prop: 'vehicleNo', label: '车号', minWidth: 120 },
{ prop: 'departureAddress', label: '发货地址', minWidth: 180 },
{ prop: 'arrivalAddress', label: '到货地址', minWidth: 180 },
{ prop: 'actualDepartureTime', label: '实际发货时间', minWidth: 170 },
{ prop: 'actualCompletionTime', label: '实际完成时间', minWidth: 170 },
{ prop: 'actualDepartureTime', label: '实际发货时间', minWidth: 170, dateTime: true },
{ prop: 'actualCompletionTime', label: '实际完成时间', minWidth: 170, dateTime: true },
{ prop: 'transportType', label: '运输类型', minWidth: 120 },
{ prop: 'cargoName', label: '货物名称', minWidth: 140 },
{ prop: 'cargoType', label: '货物类型', minWidth: 130 },
@@ -146,7 +146,7 @@
<span>{{
isTransportPlanPage
? formatTransportPlanProvinceCityDistrict(row.departureAddress)
: formatTransportPlanProvinceCityDistrict(row.departureAddress)
: formatWaybillListAddress(row.departureAddress)
}}</span>
</el-tooltip>
<span v-else>{{ row.departureAddress || '-' }}</span>
@@ -161,7 +161,7 @@
<span>{{
isTransportPlanPage
? formatTransportPlanProvinceCityDistrict(row.arrivalAddress)
: formatTransportPlanProvinceCityDistrict(row.arrivalAddress)
: formatWaybillListAddress(row.arrivalAddress)
}}</span>
</el-tooltip>
<span v-else>{{ row.arrivalAddress || '-' }}</span>
@@ -644,7 +644,7 @@
@change="handleTaskCarrierTypeChange"
>
<el-radio-button
v-for="item in taskCarrierTypes"
v-for="item in waybillTaskCarrierTypes"
:key="item"
:label="item"
:value="item"
@@ -1135,8 +1135,9 @@
<el-form-item :label="`运费${index + 1}`">
<el-input
:model-value="taskFullFreightAmount(cargo)"
disabled
placeholder="自动计算"
placeholder="请输入运费"
:disabled="dialogReadonly"
@input="value => handleTaskFullFreightAmountInput(cargo, value)"
>
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
</el-input>
@@ -1195,7 +1196,7 @@
@change="handleTaskCarrierTypeChange"
>
<el-radio-button
v-for="item in taskCarrierTypes"
v-for="item in waybillTaskCarrierTypes"
:key="item"
:label="item"
:value="item"
@@ -2054,7 +2055,7 @@
</div>
</template>
<template #menu-form-before>
<template #menu-form>
<div
v-if="showFooterFreightSummary && !dialogReadonly"
class="business-crud-page__shipping-template-footer-summary"
@@ -2070,14 +2071,16 @@
查看过程配置
</el-link>
</div>
<el-button
<div
v-if="isStandaloneFormPage && !showTransportPlanCreateActions"
@click="closeCrudDialog"
class="business-crud-page__standalone-menu-actions"
>
取消
<el-button class="business-crud-page__form-close" @click="closeCrudDialog">
{{ isWaybillDetailLayout ? '关闭' : '取消' }}
</el-button>
<el-button
v-if="config.enableDraftSave && !dialogReadonly"
class="business-crud-page__form-draft"
type="primary"
plain
:loading="draftSaveLoading"
@@ -2085,9 +2088,7 @@
>
{{ config.draftSaveText || '保存' }}
</el-button>
</template>
<template #menu-form>
</div>
<div v-if="showTransportPlanCreateActions" class="business-crud-page__create-actions">
<el-button :disabled="Boolean(transportPlanCreateActionLoading)" @click="closeCrudDialog">
取消
@@ -5418,6 +5419,7 @@ const defaultTransportCargo = () => ({
quantityUnit: '',
unitPrice: '',
priceUnit: '元/吨',
freightAmount: '',
packageType: '',
brand: '',
specification: '',
@@ -5703,6 +5705,8 @@ export default {
transportAddressTarget: '',
transportAddressLoading: false,
transportAddressQuery: {},
transportTypeOptions: [],
transportTypeOptionsLoading: false,
transportAddressRows: [],
transportAddressSelected: null,
transportAddressPage: {
@@ -5743,6 +5747,9 @@ export default {
taskCarrierOptions: [],
taskCarrierLoading: false,
taskCarrierRequestId: 0,
waybillCarrierContractsLoaded: false,
waybillHasCarrierContracts: null,
waybillCarrierContractRequestId: 0,
taskDriverOptions: [],
taskDriverLoading: false,
taskCargoOptionsMap: {},
@@ -5872,8 +5879,16 @@ export default {
if (this.config.enableWaybillFooterFreightSummary) {
this.loadShippingTemplateCurrencyOptions();
}
if (this.shippingInfoFormEnabled) this.loadTransportTypeOptions();
if (this.isStandaloneFormPage) {
this.$nextTick(() => {
this.initStandaloneFormPage();
//
this.$nextTick(() => this.consumeTemplateCreatePayload());
});
} else {
this.consumeTemplateCreatePayload();
if (this.isStandaloneFormPage) this.$nextTick(() => this.initStandaloneFormPage());
}
},
computed: {
...mapGetters(['permission', 'userInfo']),
@@ -6063,6 +6078,16 @@ export default {
taskCarrierRequired() {
return this.isTaskCarrierRequired(this.form.carrierType);
},
waybillTaskCarrierTypes() {
if (
this.isWaybillDetailLayout &&
this.waybillCarrierContractsLoaded &&
this.waybillHasCarrierContracts === false
) {
return ['自运'];
}
return this.taskCarrierTypes;
},
taskCarrierLabel() {
return this.form.carrierType === '网货平台' ? '承运方' : '承运商';
},
@@ -6636,11 +6661,36 @@ export default {
return applyTableMenuWidth(nextOption, menuButtonCount);
},
formatTransportPlanGoodsInfo(row = {}) {
const goodsRows = this.parseJsonArray(row.goodsJson);
// 使 goodsJson
const goodsRows = [
row.goodsJson,
row.goodsList,
row.goodsRows,
row.cargoList,
row.cargoRows,
row.goods,
]
.map(source => {
if (source === undefined || source === null || source === '') return [];
if (Array.isArray(source)) return source;
if (typeof source === 'object') return [source];
return this.parseJsonArray(source);
})
.filter(source => source.length)
.sort((left, right) => right.length - left.length)[0] || [];
if (!goodsRows.length) return String(row.goodsInfo || row.cargoInfo || '');
const groups = goodsRows.reduce((result, item = {}) => {
const unit =
item.quantityUnit || item.goodsQuantityUnit || item.cargoUnit || item.unit || '';
const unit = String(
item.quantityUnit ||
item.goodsQuantityUnit ||
item.cargoUnit ||
item.unit ||
row.quantityUnit ||
row.goodsQuantityUnit ||
row.cargoUnit ||
row.unit ||
''
).trim();
const key = unit || '__empty__';
if (!result[key]) {
result[key] = {
@@ -6699,6 +6749,28 @@ export default {
const cityMatch = text.match(/市/);
return cityMatch ? text.slice(0, cityMatch.index + 1) : text;
},
formatWaybillListAddress(value) {
const text = String(value || '')
.trim()
.replace(/[\\/||、,\s]+/g, '');
if (!text) return '-';
const districtPattern = '(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)';
const parts = [];
let remainder = text;
const province = text.match(/^(.+?(?:省|自治区|特别行政区))/);
if (province) {
parts.push(province[1]);
remainder = text.slice(province[1].length);
}
const city = remainder.match(/^(.+?市)/);
if (city) {
parts.push(city[1]);
remainder = remainder.slice(city[1].length);
}
const district = remainder.match(new RegExp(`^(.+?${districtPattern})`));
if (district) parts.push(district[1]);
return parts.length ? parts.join('/') : text;
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
},
@@ -6831,10 +6903,14 @@ export default {
if (payload.target !== target) return;
sessionStorage.removeItem(storageKey);
this.$nextTick(() => {
this.$refs.crud.rowAdd();
this.$nextTick(() => {
// rowAdd
if (!this.isStandaloneFormPage) this.$refs.crud?.rowAdd?.();
this.$nextTick(() => this.applyTemplateCreatePayload(payload.data));
});
},
applyTemplateCreatePayload(data) {
//
const row = { ...(payload.data || {}) };
const row = { ...(data || {}) };
[
'id',
'createUser',
@@ -6863,8 +6939,6 @@ export default {
this.loadProjectProcessConfigState();
if (this.config.enableTransportPlanForm) this.initTransportPlanFormRows();
if (this.taskInfoFormEnabled) this.initTaskInfoForm();
});
});
},
applyWaybillTemplateTaskData(template = {}) {
if (this.config.permission !== 'waybill_manage') return;
@@ -7561,6 +7635,7 @@ export default {
const submitRow = { ...row };
const skipValidation = saveAsDraft && this.config.skipDraftValidation === true;
if (
!skipValidation &&
['shipping_template', 'transport_plan', 'waybill_manage'].includes(
this.config.permission
) &&
@@ -8014,6 +8089,7 @@ export default {
handleDraftSave() {
const request =
typeof this.api.saveDraft === 'function' ? this.api.saveDraft : this.api.submit;
if (this.draftSaveLoading) return;
const submitRow = this.normalizeRow(this.form, true);
if (!submitRow) return;
this.draftSaveLoading = true;
@@ -8115,6 +8191,7 @@ export default {
this.taskCarrierRequestId += 1;
this.taskCarrierOptions = [];
this.taskCarrierLoading = false;
this.resetWaybillCarrierContractState();
this.initTaskInfoForm();
this.initTransportPlanFormRows();
this.syncBillingPlanJson();
@@ -8150,6 +8227,7 @@ export default {
this.suppressTransportTypeClear = false;
});
this.selectedProjectId = this.form.projectId || '';
this.resetWaybillCarrierContractState();
this.loadProjectProcessConfigState();
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
this.selectedAttachmentRows = [];
@@ -8271,6 +8349,7 @@ export default {
this.taskCarrierRequestId += 1;
this.taskCarrierOptions = [];
this.taskCarrierLoading = false;
this.resetWaybillCarrierContractState();
this.form.carrierName = '';
this.form.carrierId = '';
this.form.carrierContractId = '';
@@ -8282,6 +8361,9 @@ export default {
this.form.projectCode = project.projectCode || '';
this.form.undertakeDeptName = project.undertakeDeptName || '';
if (this.isWaybillDetailLayout) {
this.taskCarrierRequestId += 1;
this.taskCarrierLoading = false;
this.resetWaybillCarrierContractState();
this.form.carrierName = '';
this.form.carrierId = '';
this.form.carrierContractId = '';
@@ -8638,15 +8720,39 @@ export default {
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
this.syncTransportCargoJson();
},
handleTaskFullFreightAmountInput(cargo, value) {
this.handleTaskFullFreightNumberInput(cargo, 'freightAmount', value);
},
taskFullFreightAutoCalculable() {
const rows = this.transportCargoRows || [];
if (!rows.length) return false;
const quantityUnits = rows.map(row => String(row.quantityUnit || '').trim());
if (quantityUnits.some(unit => !unit) || new Set(quantityUnits).size !== 1) return false;
const quantityUnit = quantityUnits[0];
return rows.every(row => {
const priceUnit = String(row.priceUnit || '').trim();
const separatorIndex = Math.max(priceUnit.lastIndexOf('/'), priceUnit.lastIndexOf(''));
const priceQuantityUnit =
separatorIndex >= 0 ? priceUnit.slice(separatorIndex + 1).trim() : priceUnit;
return !this.isBillingFieldEmpty(row.unitPrice) && priceQuantityUnit === quantityUnit;
});
},
taskFullFreightQuantity(index) {
return this.formatTaskFullFreightNumber(this.transportCargoRows[index]?.quantity || 0);
},
taskFullFreightAmount(cargo = {}) {
if (!this.taskFullFreightAutoCalculable()) {
return this.formatTaskFullFreightNumber(cargo.freightAmount);
}
if (!this.isBillingFieldEmpty(cargo.freightAmount)) {
return this.formatTaskFullFreightNumber(cargo.freightAmount);
}
return this.formatTaskFullFreightNumber(
Number(cargo.unitPrice || 0) * Number(cargo.quantity || 0)
);
},
formatTaskFullFreightNumber(value) {
if (value === undefined || value === null || String(value).trim() === '') return '';
const number = Number(value || 0);
if (!Number.isFinite(number)) return '';
return Number(number.toFixed(2)).toString();
@@ -8668,30 +8774,74 @@ export default {
if (this.isWaybillDetailLayout) {
this.taskCarrierRequestId += 1;
this.taskCarrierOptions = [];
this.taskCarrierLoading = false;
}
},
isTaskCarrierRequired(carrierType) {
return ['承运商', '网货平台'].includes(String(carrierType || '').trim());
},
loadTaskCarrierOptions(project = {}) {
if (!this.taskInfoFormEnabled || this.taskCarrierLoading) return Promise.resolve([]);
if (this.isWaybillDetailLayout) {
resetWaybillCarrierContractState() {
if (!this.isWaybillDetailLayout) return;
this.waybillCarrierContractRequestId += 1;
this.waybillCarrierContractsLoaded = false;
this.waybillHasCarrierContracts = null;
},
loadWaybillCarrierContractState(project = {}) {
if (!this.isWaybillDetailLayout) return Promise.resolve(null);
const projectId = project.id || project.projectId || this.form.projectId;
const requestId = ++this.taskCarrierRequestId;
if (this.form.carrierType === '承运商') {
const requestId = ++this.waybillCarrierContractRequestId;
if (!projectId) {
this.taskCarrierOptions = [];
this.waybillCarrierContractsLoaded = false;
this.waybillHasCarrierContracts = null;
return Promise.resolve([]);
}
this.taskCarrierLoading = true;
return getContractList(1, 9999, {
projectId,
projectName: project.projectName || this.form.projectName,
contractCategory: '承运商合同',
})
.then(res => {
if (requestId !== this.waybillCarrierContractRequestId) return null;
const options = this.getWaybillCarrierContractOptions(extractRecords(res));
this.waybillCarrierContractsLoaded = true;
this.waybillHasCarrierContracts = options.length > 0;
if (!options.length && this.form.carrierType !== '自运') {
this.form.carrierType = '自运';
this.form.carrierName = '';
this.form.carrierId = '';
this.form.carrierContractId = '';
}
return options;
})
.catch(() => {
if (requestId === this.waybillCarrierContractRequestId) {
this.waybillCarrierContractsLoaded = false;
this.waybillHasCarrierContracts = null;
}
return null;
});
},
loadTaskCarrierOptions(project = {}) {
if (!this.taskInfoFormEnabled || this.taskCarrierLoading) return Promise.resolve([]);
if (this.isWaybillDetailLayout) {
const projectId = project.id || project.projectId || this.form.projectId;
const requestId = ++this.taskCarrierRequestId;
const carrierTypeAtRequest = this.form.carrierType;
const contractPromise = this.loadWaybillCarrierContractState({
...project,
id: projectId,
});
if (carrierTypeAtRequest === '承运商') {
if (!projectId) {
this.taskCarrierOptions = [];
return Promise.resolve([]);
}
this.taskCarrierLoading = true;
return contractPromise
.then(options => {
if (requestId !== this.taskCarrierRequestId) return this.taskCarrierOptions;
this.taskCarrierOptions = this.getWaybillCarrierContractOptions(extractRecords(res));
if (options === null) return this.taskCarrierOptions;
this.taskCarrierOptions = options || [];
return this.taskCarrierOptions;
})
.catch(() => this.taskCarrierOptions)
@@ -8708,7 +8858,7 @@ export default {
});
if (!projectId) return Promise.resolve(this.taskCarrierOptions);
this.taskCarrierLoading = true;
return getProjectDetail(projectId)
const detailPromise = getProjectDetail(projectId)
.then(res => {
if (requestId !== this.taskCarrierRequestId) return this.taskCarrierOptions;
const detail = res?.data?.data || {};
@@ -8719,7 +8869,9 @@ export default {
});
return this.taskCarrierOptions;
})
.catch(() => this.taskCarrierOptions)
.catch(() => this.taskCarrierOptions);
return Promise.all([contractPromise, detailPromise])
.then(([, options]) => options)
.finally(() => {
if (requestId === this.taskCarrierRequestId) this.taskCarrierLoading = false;
});
@@ -8974,12 +9126,16 @@ export default {
},
initTaskFullCargoRows(goodsRows = []) {
if (!this.taskInfoFormEnabled) return;
const freightItems = this.parseJsonObject(this.form.freightJson).freightItems || [];
const rows = (goodsRows.length ? goodsRows : this.transportCargoRows).map((row, index) => {
const cargo = this.normalizeTransportCargoRow(row);
if (index === 0 && !cargo.unitPrice && this.form.unitPrice) {
cargo.unitPrice = this.form.unitPrice;
}
cargo.priceUnit = cargo.priceUnit || this.form.priceUnit || '元/吨';
if (this.isBillingFieldEmpty(cargo.freightAmount)) {
cargo.freightAmount = freightItems[index]?.freightAmount ?? '';
}
return cargo;
});
while (rows.length < 1) {
@@ -9076,6 +9232,10 @@ export default {
this.$message.warning(`${index + 1}行单价不能小于0`);
return false;
}
if (!this.isBillingFieldEmpty(cargo.freightAmount) && Number(cargo.freightAmount) < 0) {
this.$message.warning(`${index + 1}行运费不能小于0`);
return false;
}
}
return true;
},
@@ -9328,6 +9488,7 @@ export default {
cargoName: row.cargoName || row.goodsName || '',
packageType: row.packageType || row.package || '',
specification: row.specification || row.spec || '',
freightAmount: row.freightAmount ?? row.amount ?? row.totalAmount ?? '',
};
['quantity', 'mileage', 'unitPrice'].forEach(prop => {
if (Number(cargo[prop]) === -1) cargo[prop] = '';
@@ -9470,8 +9631,11 @@ export default {
]);
},
getTransportTypeLabel(value) {
if (value && typeof value === 'object') {
return value.label || value.dictValue || value.name || value.value || value.dictKey || '';
}
const column = (this.option.column || []).find(item => item.prop === 'transportType');
const dictionary = column?.dicData || [];
const dictionary = [...(column?.dicData || []), ...this.transportTypeOptions];
const item = dictionary.find(
dic =>
String(dic.value ?? dic.dictKey ?? '') === String(value ?? '') ||
@@ -9479,11 +9643,28 @@ export default {
);
return item?.label || item?.dictValue || '';
},
loadTransportTypeOptions() {
if (this.transportTypeOptions.length || this.transportTypeOptionsLoading) {
return Promise.resolve(this.transportTypeOptions);
}
this.transportTypeOptionsLoading = true;
return getDictionary({ code: 'transport_type' })
.then(res => {
this.transportTypeOptions = this.normalizeDictOptions(res.data?.data || []);
return this.transportTypeOptions;
})
.finally(() => {
this.transportTypeOptionsLoading = false;
});
},
resolveTransportMode(value) {
const text = [value, this.form.transportTypeName, this.getTransportTypeLabel(value)]
.filter(Boolean)
.join(' ')
.toLowerCase();
const values = [value, this.form.transportTypeName, this.getTransportTypeLabel(value)]
.flatMap(item => {
if (!item || typeof item !== 'object') return [item];
return [item.label, item.dictValue, item.name, item.value, item.dictKey];
})
.filter(Boolean);
const text = values.join(' ').toLowerCase();
if (!text) return '';
if (
text.includes('公路') ||
@@ -9715,17 +9896,28 @@ export default {
},
openTransportAddressDialog(target) {
if (!this.ensureTransportTypeBeforeAddress()) return;
const open = () => {
this.transportAddressTarget = target;
this.transportAddressSelected = null;
this.resetTransportAddressQuery();
this.transportAddressPage.currentPage = 1;
this.transportAddressBox = true;
};
// Avue option
if (this.isTransportPlanPage && !this.getTransportFixedAddressType()) {
this.loadTransportTypeOptions().finally(open);
return;
}
open();
},
openTransportAddressPicker(target) {
if (
this.transportStationMode &&
!(this.isTransportPlanPage && this.transportMode === 'water')
) {
// 使
// /
if (this.isTransportPlanPage) {
this.openTransportAddressDialog(target);
return;
}
if (this.transportStationMode) {
this.openTransportStationDialog(target);
return;
}
@@ -9808,7 +10000,10 @@ export default {
this.form[`${prefix}Latitude`] = address.latitude || '';
this.form[`${prefix}Contact`] = address.contactName || this.form[`${prefix}Contact`] || '';
this.form[`${prefix}Phone`] = address.contactPhone || this.form[`${prefix}Phone`] || '';
this.$refs.crud?.validateField?.(`${prefix}Address`);
// Avue
this.$nextTick(() => {
this.$refs.crud?.clearValidate?.([`${prefix}Address`]);
});
},
openTransportMapDialog(target) {
if (this.dialogReadonly) return;
@@ -9974,7 +10169,9 @@ export default {
this.form[`${prefix}Longitude`] = this.transportMapSelected.longitude;
this.form[`${prefix}Latitude`] = this.transportMapSelected.latitude;
this.transportMapBox = false;
this.$refs.crud?.validateField?.(`${prefix}Address`);
this.$nextTick(() => {
this.$refs.crud?.clearValidate?.([`${prefix}Address`]);
});
},
buildTransportMapSelection({ lng, lat, address, regionName, regionCode }) {
const longitude = this.formatCoordinate(lng);
@@ -11721,13 +11918,17 @@ export default {
this.api.getDetail(row.id).then(res => {
const template = res.data?.data || row;
const templateType = template.templateType || template.templateTypeName;
const target =
const listTarget =
templateType === '运单' ? '/business/waybill-manage' : '/business/transport-plan';
const target = standaloneBusinessFormRoutes[listTarget] || listTarget;
sessionStorage.setItem(
'business-template-create',
JSON.stringify({ target: target.slice('/business/'.length), data: template })
JSON.stringify({ target: listTarget.slice('/business/'.length), data: template })
);
this.$router.push({ path: target, query: { fromTemplate: Date.now().toString() } });
this.$router.push({
path: target,
query: { mode: 'add', fromTemplate: Date.now().toString() },
});
});
return;
}
@@ -15535,10 +15736,21 @@ export default {
:global(.business-crud-form-page-dialog .business-crud-page__shipping-template-footer-summary) {
position: static;
order: 1;
height: auto;
margin-right: auto;
}
:global(.business-crud-form-page-dialog .avue-form__menu > .business-crud-page__standalone-menu-actions) {
display: flex;
order: 2;
gap: 8px;
}
:global(.business-crud-form-page-dialog .avue-form__menu > .el-button) {
order: 3;
}
:global(.avue--collapse .el-overlay:has(.business-crud-form-page-dialog)) {
left: 60px !important;
}
@@ -154,10 +154,10 @@
<template v-else>
<el-col v-if="route.carrierType === '承运商'" :span="6"><el-form-item label="承运商" required><el-select v-model="route.carrierContractId" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="item.contractId" :label="carrierOptionLabel(item)" :value="item.contractId" /></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="船长" required><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.captainName" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="联系电话" required><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="箱号" required><el-input v-model="route.containerNo" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="舱位" required><el-input v-model="route.cabinNo" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="箱号"><el-input v-model="route.containerNo" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="舱位"><el-input v-model="route.cabinNo" placeholder="请输入" /></el-form-item></el-col>
</template>
<el-col :span="6"><el-form-item label="里程(km"><el-input :model-value="route.mileage" inputmode="numeric" maxlength="10" placeholder="请输入" @input="value => handleMileageInput(route, value)" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="备注"><el-input v-model="route.remark" maxlength="200" show-word-limit placeholder="请输入" /></el-form-item></el-col>
@@ -316,6 +316,10 @@ export default {
this.syncFreightItems(route);
},
isRoad(route) { const type = String(route.transportType || '').toLowerCase(); return type.includes('公路') || type === 'road'; },
isWater(route) {
const type = String(route.transportType || '').trim().toLowerCase();
return type === 'river' || type === 'water' || type === 'sl' || type.includes('水路') || type.includes('水运');
},
dispatchedQuantity(route) {
return this.quantityNumber(route.dispatchedQuantity) + this.pendingSegmentQuantity(route);
},
@@ -619,7 +623,7 @@ export default {
if (this.isRoad(route)) {
if (route.carrierType === '承运商' && (!route.carrierContractId || !route.carrierName || !route.vehicleNo)) return this.$message.warning('请选择承运商并填写车牌号');
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo || !route.trailerVehicleNo || !route.escortName || !route.escortPhone)) return this.$message.warning('请补全自运或网货平台的车辆与人员信息');
} else if (!route.vehicleNo || !route.captainName || !route.driverPhone || !route.containerNo || !route.cabinNo || (route.carrierType === '承运商' && (!route.carrierContractId || !route.carrierName))) {
} else if (!route.vehicleNo || !route.driverPhone || (route.carrierType === '承运商' && (!route.carrierContractId || !route.carrierName))) {
return this.$message.warning('请补全非公路运输的承运信息');
}
}
@@ -29,12 +29,12 @@
:label="contract.contractName"
:value="contract.id" /></el-select></el-form-item></el-col
><el-col :span="6"
><el-form-item label="总单号"
><el-form-item label="总单号" required
><el-input
:model-value="form.masterNo || '系统自动生成'"
readonly /></el-form-item></el-col
><el-col :span="6"
><el-form-item label="运输组织类型"
><el-form-item label="运输组织类型" required
><el-select v-model="form.transportOrganizationType" disabled
><el-option label="多式联运" value="多式联运" /></el-select></el-form-item></el-col
><el-col :span="24"
@@ -1766,6 +1766,10 @@ export default {
};
</script>
<style scoped lang="scss">
.master-editor {
padding-bottom: 80px;
}
.master-editor section {
margin-bottom: 12px;
padding: 16px;
+29 -8
View File
@@ -247,13 +247,14 @@
:min="0"
:precision="2"
:controls="false"
@change="recalculateLine(row)"
/>
</template>
</el-table-column>
<el-table-column label="含税金额" min-width="130">
<el-table-column label="含税单价" min-width="130">
<template #default="{ row }">
<el-input-number
v-model="row.amountWithTax"
v-model="row.unitPriceNoTax"
:disabled="readonly"
:min="0"
:precision="2"
@@ -262,6 +263,9 @@
/>
</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
@@ -281,6 +285,9 @@
<el-table-column label="税额" min-width="120">
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
</el-table-column>
<el-table-column label="合计" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.totalAmount) }}</template>
</el-table-column>
<el-table-column label="备注" min-width="170">
<template #default="{ row }">
<el-input v-model="row.remark" :disabled="readonly" maxlength="200" />
@@ -374,6 +381,7 @@
<span v-else-if="column.prop === 'transportType'">{{
transportTypeLabel(row.transportType)
}}</span>
<span v-else-if="column.dateTime">{{ formatDateTime(row[column.prop]) }}</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
@@ -492,7 +500,9 @@ const emptyLine = () => ({
unit: '吨',
quantity: 0,
unitPriceNoTax: 0,
amountNoTax: 0,
amountWithTax: 0,
totalAmount: 0,
taxRate: 0,
taxAmount: 0,
remark: '',
@@ -598,7 +608,7 @@ export default {
return this.form.sheets.reduce(
(sheetTotal, sheet) =>
sheetTotal +
sheet.lines.reduce((lineTotal, line) => lineTotal + Number(line.amountWithTax || 0), 0),
sheet.lines.reduce((lineTotal, line) => lineTotal + Number(line.totalAmount || 0), 0),
0
);
},
@@ -714,11 +724,11 @@ export default {
const item = this.findInvoiceItem(line);
if (item) {
line.taxRate = Number(item.defaultTaxRate || 0);
this.recalculateLine(line);
} else if (clearInvalid) {
line.goodsName = '';
line.taxRate = 0;
}
this.recalculateLine(line);
},
handleGoodsCategoryChange(line) {
const names = this.invoiceItemNames(line.goodsCategory);
@@ -1067,9 +1077,14 @@ export default {
sheet.lines.push(emptyLine());
},
recalculateLine(row) {
const amount = Number(row.amountWithTax || 0);
const quantity = Number(row.quantity || 0);
const unitPrice = Number(row.unitPriceNoTax || 0);
const amountNoTax = Number((quantity * unitPrice).toFixed(2));
const rate = Number(row.taxRate || 0) / 100;
row.taxAmount = rate ? Number((amount - amount / (1 + rate)).toFixed(2)) : 0;
row.amountNoTax = amountNoTax;
row.taxAmount = rate ? Number((amountNoTax * rate).toFixed(2)) : 0;
row.totalAmount = Number((amountNoTax + row.taxAmount).toFixed(2));
row.amountWithTax = row.totalAmount;
},
validateBusiness() {
if (!this.selectedDetailIds.length) throw new Error('请至少选择一条开票明细');
@@ -1082,7 +1097,7 @@ export default {
for (const line of sheet.lines) {
if (!line.goodsCategory || !line.goodsName) throw new Error('请完整填写商品和服务信息');
if (
[line.quantity, line.unitPriceNoTax, line.amountWithTax, line.taxRate].some(
[line.quantity, line.unitPriceNoTax, line.amountNoTax, line.taxRate].some(
Number.isNaN
)
) {
@@ -1121,7 +1136,9 @@ export default {
unit: line.unit,
quantity: line.quantity,
unitPriceNoTax: line.unitPriceNoTax,
amountWithTax: line.amountWithTax,
amountNoTax: line.amountNoTax,
totalAmount: line.totalAmount,
amountWithTax: line.totalAmount,
taxRate: line.taxRate,
taxAmount: line.taxAmount,
remark: line.remark,
@@ -1169,6 +1186,10 @@ export default {
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatDateTime(value) {
if (!value) return '-';
return this.$dayjs(value).format('YYYY-MM-DD HH:mm:ss');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
+1 -1
View File
@@ -654,7 +654,7 @@ export default {
...item,
formalSettlementId: item.id,
settlementId: item.id,
allocatedInvoiceAmount: 0,
allocatedInvoiceAmount: rows.length === 1 ? Number(item.settlementAmount || 0) : 0,
}));
this.form.projectName = first.projectName;
this.form.deptName = first.deptName;
+152 -39
View File
@@ -140,23 +140,14 @@
></el-col>
<el-col v-if="billPayment" :span="6"
><el-form-item label="汇票单号" prop="billLedgerId"
><el-select
v-model="form.billLedgerId"
><el-input
v-model="form.billNo"
readonly
:disabled="readonly"
filterable
remote
reserve-keyword
:remote-method="loadBillOptions"
:loading="billLoading"
placeholder="请选择可用汇票"
@change="handleBillChange"
><el-option
v-for="item in billOptions"
:key="item.id"
:label="`${item.billNo}|余额${formatMoney(item.availableBalance)}${
item.maturityDate
}`"
:value="item.id" /></el-select></el-form-item
><template #append
><el-button :disabled="readonly" @click="openBillDialog">选择</el-button></template
></el-input></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="收款方"
@@ -501,6 +492,54 @@
</el-tab-pane>
</el-tabs></div
></el-dialog>
<el-dialog v-model="billDialogVisible" title="选择汇票" width="80%" append-to-body>
<div class="payment-form-page__reference-search">
<el-form :model="billQuery" label-position="right" label-width="160px" @submit.prevent>
<div class="payment-form-page__reference-search-fields">
<el-form-item label="汇票筛选">
<el-input
v-model="billQuery.keyword"
clearable
placeholder="请输入汇票号、出票单位或收票单位"
@keyup.enter="handleBillSearch"
/>
</el-form-item>
</div>
<div class="payment-form-page__reference-search-actions">
<el-button type="primary" @click="handleBillSearch">搜索</el-button>
<el-button @click="resetBillSearch">清空</el-button>
</div>
</el-form>
</div>
<el-table v-loading="billPage.loading" :data="billRows" border>
<el-table-column prop="billNo" label="汇票单号" min-width="180" />
<el-table-column prop="issuerName" label="出票单位" min-width="160" />
<el-table-column prop="receiverName" label="收票单位" min-width="160" />
<el-table-column prop="faceAmount" label="票面金额" min-width="120">
<template #default="{ row }">{{ formatMoney(row.faceAmount) }}</template>
</el-table-column>
<el-table-column prop="availableBalance" label="可用余额" min-width="120">
<template #default="{ row }">{{ formatMoney(row.availableBalance) }}</template>
</el-table-column>
<el-table-column prop="maturityDate" label="到期日期" min-width="120" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-link type="primary" @click="selectBill(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<div class="payment-form-page__reference-pagination">
<el-pagination
v-model:current-page="billPage.current"
v-model:page-size="billPage.size"
:total="billPage.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadBillPage"
@size-change="handleBillSizeChange"
/>
</div>
</el-dialog>
<el-dialog
v-model="attachmentDocumentPreviewVisible"
:title="attachmentPreviewFile.name || '附件预览'"
@@ -552,6 +591,7 @@ import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/paymentApplication';
import * as billLedgerApi from '@/api/payment/billLedger';
import { getDictionary } from '@/api/system/dictbiz';
import { getList as getProjectList } from '@/api/business/project-apply';
import { getDetail as getContractDetail } from '@/api/business/contract-manage';
import * as formalApi from '@/api/settlement/formalSettlement';
@@ -674,6 +714,17 @@ export default {
contractRows: [],
billOptions: [],
billLoading: false,
billDialogVisible: false,
billQuery: {
keyword: '',
},
billRows: [],
billPage: {
current: 1,
size: 10,
total: 0,
loading: false,
},
receiptAccountOptions: [],
receiptAccountLoading: false,
firstContractPayment: false,
@@ -696,7 +747,7 @@ export default {
zoom: true,
},
paymentTypeOptions: api.paymentTypeOptions,
paymentMethodOptions: api.paymentMethodOptions,
paymentMethodOptions: [],
attachmentFileTypes: [
'pdf',
'bmp',
@@ -740,7 +791,7 @@ export default {
return this.hasPermission(code);
},
billPayment() {
return this.form.paymentMethod !== 'bank_transfer';
return this.isBillPaymentMethod();
},
paymentAmountBase() {
if (this.form.paymentType === 'project_advance') return 0;
@@ -976,6 +1027,7 @@ export default {
return body?.data || body;
},
async initialize() {
await this.loadPaymentMethodOptions();
if (this.recordId) {
const data = this.unwrapData(await api.getDetail(this.recordId));
this.form = {
@@ -1020,6 +1072,37 @@ export default {
? paymentType
: 'project_advance';
},
async loadPaymentMethodOptions() {
try {
const data = this.unwrapData(await getDictionary({ code: 'pay_method' }));
const records = Array.isArray(data) ? data : data?.records || [];
this.paymentMethodOptions = records
.map(item => ({
...item,
label: item.dictValue || item.label || item.name || item.dictKey || item.value,
value: item.dictKey || item.value || item.dictValue || item.name,
}))
.filter(item => item.label && item.value);
} catch (error) {
this.paymentMethodOptions = [];
this.$message.warning('付款方式字典加载失败,请稍后重试');
}
},
isBillPaymentMethod(value = this.form.paymentMethod) {
const selected = this.paymentMethodOptions.find(
item => String(item.value) === String(value)
);
const searchableText = [
selected?.dictKey,
selected?.dictValue,
selected?.label,
selected?.name,
selected?.value,
]
.filter(Boolean)
.join(' ');
return searchableText.includes('汇票');
},
getTransferredPreSettlementIds() {
const payloadIds = (this.transferPayload?.sourcePreSettlements || []).map(
item => item.preSettlementId || item.id
@@ -1566,14 +1649,13 @@ export default {
}
this.contractPaymentRatioExceeded = exceeded;
},
validateRequiredAttachments() {
if (!this.missingAttachmentMaterials.length) return true;
notifyMissingAttachments() {
if (!this.missingAttachmentMaterials.length) return;
this.$message.warning(
`请先上传付款申请所需材料:${this.missingAttachmentMaterials
`付款申请所需材料未上传,请核对${this.missingAttachmentMaterials
.map(item => item.label)
.join('、')}`
);
return false;
},
handleTypeChange() {
this.form.invoices = [];
@@ -1648,20 +1730,10 @@ export default {
try {
const response = await formalApi.getContractOptions('', projectId);
this.contractOptions = (this.unwrapData(response) || []).filter(
item => String(item.projectId || '') === String(projectId)
item =>
String(item.projectId || '') === String(projectId) &&
String(item.contractCategory || '').trim() === '承运商合同'
);
if (
this.form.contractId &&
this.form.contractName &&
!this.contractOptions.some(item => String(item.id) === String(this.form.contractId))
) {
this.contractOptions.unshift({
id: this.form.contractId,
contractNo: this.form.contractNo,
contractName: this.form.contractName,
projectId,
});
}
} catch (error) {
this.contractOptions = [];
this.$message.warning('项目合同加载失败,请稍后重试');
@@ -1807,6 +1879,8 @@ export default {
this.form.billLedgerId = null;
this.form.billNo = '';
this.billOptions = [];
this.billRows = [];
this.billDialogVisible = false;
return;
}
await this.loadBillOptions();
@@ -1823,10 +1897,49 @@ export default {
this.billLoading = false;
}
},
handleBillChange(id) {
const selected = this.billOptions.find(item => String(item.id) === String(id));
this.form.billNo = selected?.billNo || '';
this.$refs.formRef?.validateField('billLedgerId').catch(() => {});
async openBillDialog() {
if (this.readonly || !this.billPayment) return;
this.billDialogVisible = true;
this.billQuery.keyword = '';
this.billPage.current = 1;
await this.loadBillPage();
},
async loadBillPage() {
if (!this.billPayment) return;
this.billPage.loading = true;
try {
const response = await billLedgerApi.getAvailablePage(
this.billPage.current,
this.billPage.size,
String(this.billQuery.keyword || '').trim() || undefined,
this.form.deptId,
this.form.billLedgerId
);
const data = this.unwrapData(response) || {};
this.billRows = data.records || [];
this.billPage.total = Number(data.total || 0);
} finally {
this.billPage.loading = false;
}
},
handleBillSearch() {
this.billPage.current = 1;
this.loadBillPage();
},
resetBillSearch() {
this.billQuery.keyword = '';
this.handleBillSearch();
},
handleBillSizeChange() {
this.billPage.current = 1;
this.loadBillPage();
},
selectBill(row) {
this.form.billLedgerId = row.id;
this.form.billNo = row.billNo || '';
this.billOptions = [row];
this.billDialogVisible = false;
this.$nextTick(() => this.$refs.formRef?.validateField('billLedgerId').catch(() => {}));
},
referenceParams(type) {
const { settlementNo, projectName, contractName } = this.referenceQuery;
@@ -2200,7 +2313,7 @@ export default {
this.$message.warning('请选择所属项目');
return false;
}
if (validateMaterials && !this.validateRequiredAttachments()) return false;
if (validateMaterials) this.notifyMissingAttachments();
try {
this.validateInvoices();
this.validatePaymentRecords();
@@ -0,0 +1,447 @@
<template>
<basic-container class="receipt-claim-batch-form-page">
<div class="archive-page-form__title">批量认领收款流水</div>
<el-form ref="formRef" :model="form" class="receipt-claim-batch-form-page__form">
<section-card title="收款信息">
<el-table v-loading="loading" :data="flows" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
prop="receiptNoticeNo"
label="认领通知单"
min-width="160"
show-overflow-tooltip
/>
<el-table-column prop="payerName" label="付款人" min-width="150" show-overflow-tooltip />
<el-table-column label="收款金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.receiptAmount) }}</template>
</el-table-column>
<el-table-column prop="transactionTime" label="交易时间" min-width="170" />
<el-table-column
prop="counterpartyName"
label="对方户名"
min-width="150"
show-overflow-tooltip
/>
<el-table-column
prop="counterpartyBank"
label="对方开户行"
min-width="170"
show-overflow-tooltip
/>
<el-table-column
prop="counterpartyAccount"
label="对方账号"
min-width="180"
show-overflow-tooltip
/>
<el-table-column
prop="detailSerialNo"
label="明细流水号"
min-width="180"
show-overflow-tooltip
/>
<el-table-column label="已认领金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.claimedAmount) }}</template>
</el-table-column>
<el-table-column prop="summary" label="摘要" min-width="180" show-overflow-tooltip />
</el-table>
<el-row :gutter="32" class="receipt-claim-batch-form-page__claim-info">
<el-col :span="6">
<el-form-item label="认领人">
<el-input v-model="claimerName" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="认领日期">
<el-input v-model="claimDate" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="认领人部门">
<el-input v-model="claimerDeptName" disabled />
</el-form-item>
</el-col>
</el-row>
</section-card>
<section-card title="结算信息">
<div class="receipt-claim-batch-form-page__settlement-toolbar">
<el-button type="primary" @click="openSettlementDialog">选择结算单</el-button>
</div>
<el-table
:data="allocationRows"
border
show-summary
:summary-method="summaryMethod"
:span-method="settlementSpanMethod"
empty-text="请先选择应收正式结算单"
>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="结算单号" min-width="180">
<template #default="{ row }">{{ row.settlement.formalSettlementNo || '-' }}</template>
</el-table-column>
<el-table-column label="结算总金额" min-width="140" align="right">
<template #default="{ row }">{{
formatMoney(row.settlement.settlementAmount)
}}</template>
</el-table-column>
<el-table-column label="已认领收款金额" min-width="150" align="right">
<template #default="{ row }">{{
formatMoney(row.settlement.claimedReceiptAmount)
}}</template>
</el-table-column>
<el-table-column label="认领通知单" min-width="180">
<template #default="{ row }">{{ row.flow.receiptNoticeNo || '-' }}</template>
</el-table-column>
<el-table-column label="收款金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.flow.receiptAmount) }}</template>
</el-table-column>
<el-table-column label="分摊收款金额" min-width="180" align="right">
<template #default="{ row }">
<el-input-number
v-model="row.settlement.allocations[row.flow.id]"
:min="0"
:max="maxAllocation(row)"
:precision="2"
:controls="false"
@change="handleAllocationChange(row.settlement)"
/>
</template>
</el-table-column>
</el-table>
</section-card>
<section-card title="附件信息">
<vehicle-attachment-table v-model="form.attachments" />
</section-card>
<section-card title="备注">
<el-form-item class="receipt-claim-batch-form-page__remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
/>
</el-form-item>
</section-card>
<div class="receipt-claim-batch-form-page__actions">
<el-button :disabled="submitting" @click="goBack">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submitClaim">确认</el-button>
</div>
</el-form>
<el-dialog
v-model="settlementDialog.visible"
title="选择应收正式结算单"
width="86%"
append-to-body
>
<div class="receipt-claim-batch-form-page__dialog-search">
<el-input
v-model="settlementDialog.keyword"
clearable
placeholder="结算单号、项目或合同"
@keyup.enter="loadSettlementCandidates"
/>
<el-button type="primary" @click="loadSettlementCandidates">查询</el-button>
</div>
<el-table v-loading="settlementDialog.loading" :data="settlementDialog.rows" border>
<el-table-column prop="formalSettlementNo" label="结算单号" min-width="170" />
<el-table-column prop="projectName" label="所属项目" min-width="150" />
<el-table-column prop="deptName" label="所属组织" min-width="150" />
<el-table-column prop="payerName" label="付款方" min-width="150" />
<el-table-column prop="payeeName" label="收款方" min-width="150" />
<el-table-column label="结算总金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
</el-table-column>
<el-table-column label="已认领收款金额" min-width="150" align="right">
<template #default="{ row }">{{ formatMoney(row.claimedReceiptAmount) }}</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click="selectSettlement(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="settlementDialog.visible = false">取消</el-button>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import * as api from '@/api/payment/receiptFlow';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
const emptyForm = () => ({ attachments: [], remark: '' });
export default {
name: 'ReceiptFlowBatchForm',
components: { VehicleAttachmentTable },
data() {
return {
form: emptyForm(),
flows: [],
settlements: [],
loading: false,
submitting: false,
claimerName: '',
claimerDeptName: '',
claimDate: '',
settlementDialog: { visible: false, loading: false, keyword: '', rows: [], selection: [] },
};
},
created() {
const userInfo = this.$store.getters.userInfo || {};
this.claimerName = userInfo.realName || userInfo.userName || '';
this.claimerDeptName = userInfo.deptName || userInfo.dept_name || '';
this.claimDate = this.$dayjs().format('YYYY-MM-DD');
this.initialize();
},
computed: {
allocationRows() {
return this.settlements.flatMap(settlement => this.flows.map(flow => ({ settlement, flow })));
},
},
methods: {
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async initialize() {
const ids = [
...new Set(
String(this.$route.query.ids || '')
.split(',')
.filter(Boolean)
),
];
if (!ids.length) {
this.$message.error('缺少批量认领流水');
this.goBack();
return;
}
this.loading = true;
try {
const details = await Promise.all(ids.map(id => api.getDetail(id)));
this.flows = details.map(item => this.unwrapData(item)).filter(Boolean);
if (!this.flows.length) throw new Error('没有可认领的收款流水');
const first = this.flows[0];
const sameParty = this.flows.every(
row =>
String(row.payerName || '').trim() === String(first.payerName || '').trim() &&
String(row.counterpartyAccount || '').trim() ===
String(first.counterpartyAccount || '').trim()
);
if (!sameParty) throw new Error('批量认领所选流水的付款人、对方账号必须一致');
} catch (error) {
this.$message.error(error?.message || '批量认领流水加载失败');
this.goBack();
} finally {
this.loading = false;
}
},
async openSettlementDialog() {
this.settlementDialog.visible = true;
await this.loadSettlementCandidates();
},
async loadSettlementCandidates() {
if (!this.flows.length) return;
this.settlementDialog.loading = true;
try {
this.settlementDialog.rows =
this.unwrapData(
await api.getSettlementCandidates(this.settlementDialog.keyword, this.flows[0].id)
) || [];
} finally {
this.settlementDialog.loading = false;
}
},
selectSettlement(row) {
this.settlementDialog.selection = [row];
this.confirmSettlements();
},
confirmSettlements() {
const rows = this.settlementDialog.selection;
if (!rows.length) {
this.$message.warning('请至少选择一张应收正式结算单');
return;
}
const item = rows[0];
this.settlements = [
{
...item,
allocations: this.flows.reduce((result, flow) => {
result[flow.id] = 0;
return result;
}, {}),
},
];
this.settlementDialog.visible = false;
},
settlementRemaining(row) {
return Math.max(Number(row.settlementAmount || 0) - Number(row.claimedReceiptAmount || 0), 0);
},
settlementAllocatedTotal(row) {
return this.flows.reduce((total, flow) => total + Number(row.allocations?.[flow.id] || 0), 0);
},
flowAllocatedTotal(flowId) {
return this.settlements.reduce(
(total, row) => total + Number(row.allocations?.[flowId] || 0),
0
);
},
maxAllocation(row) {
const current = Number(row.settlement.allocations?.[row.flow.id] || 0);
const otherTotal = this.settlementAllocatedTotal(row.settlement) - current;
return Math.max(this.settlementRemaining(row.settlement) - otherTotal, 0);
},
handleAllocationChange(settlement) {
const total = this.settlementAllocatedTotal(settlement);
if (total > this.settlementRemaining(settlement) + 0.001) {
this.$message.warning(`结算单${settlement.formalSettlementNo}的分摊金额不能超过可认领金额`);
}
},
summaryMethod({ columns }) {
return columns.map((column, index) => {
if (index === 0) return '合计';
if (index === 6) return this.formatMoney(this.allocatedTotal());
return '';
});
},
settlementSpanMethod({ rowIndex, columnIndex }) {
if (![1, 2, 3].includes(columnIndex)) return [1, 1];
if (!this.flows.length) return [1, 1];
if (rowIndex % this.flows.length === 0) return [this.flows.length, 1];
return [0, 0];
},
allocatedTotal() {
return this.settlements.reduce((total, row) => total + this.settlementAllocatedTotal(row), 0);
},
async submitClaim() {
if (!this.settlements.length) {
this.$message.warning('请至少选择一张应收正式结算单');
return;
}
for (const row of this.settlements) {
if (this.settlementAllocatedTotal(row) > this.settlementRemaining(row) + 0.001) {
this.$message.warning(`结算单${row.formalSettlementNo}的累计认领金额不能超过结算总金额`);
return;
}
}
for (const flow of this.flows) {
const total = this.flowAllocatedTotal(flow.id);
const remaining = Math.max(
Number(flow.receiptAmount || 0) - Number(flow.claimedAmount || 0),
0
);
if (total > remaining + 0.001) {
this.$message.warning(`流水${flow.receiptNoticeNo}的分摊金额不能超过剩余可认领金额`);
return;
}
}
const requests = this.flows
.map(flow => ({
flow,
settlements: this.settlements
.map(row => ({
settlementId: row.id,
allocatedReceiptAmount: Number(row.allocations?.[flow.id] || 0),
}))
.filter(row => row.allocatedReceiptAmount > 0),
}))
.filter(item => item.settlements.length);
if (!requests.length) {
this.$message.warning('没有需要入库的分摊收款金额');
return;
}
this.submitting = true;
try {
for (const item of requests) {
await api.claim({
flowId: item.flow.id,
attachmentsJson: JSON.stringify(this.form.attachments || []),
remark: this.form.remark,
settlements: item.settlements,
});
}
this.$message.success(`批量认领成功,共${requests.length}条流水`);
this.goBack();
} finally {
this.submitting = false;
}
},
goBack() {
this.$router.push('/payment/receipt-flow');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.receipt-claim-batch-form-page__form {
padding-bottom: 72px;
}
.receipt-claim-batch-form-page__claim-info {
margin-top: 16px;
}
.receipt-claim-batch-form-page__form :deep(.el-input-number) {
width: 100%;
}
.receipt-claim-batch-form-page__settlement-toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
color: #606266;
}
.receipt-claim-batch-form-page__notice-list {
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
}
.receipt-claim-batch-form-page__actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 24px;
position: fixed;
right: 0;
left: 230px;
bottom: 0;
z-index: 10;
background: #fff;
border-top: 1px solid #eff1f7;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
}
.receipt-claim-batch-form-page__remark :deep(.el-form-item__content) {
margin-left: 0 !important;
}
.receipt-claim-batch-form-page__dialog-search {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 12px;
}
.receipt-claim-batch-form-page__dialog-search .el-input {
width: 360px;
}
.receipt-claim-batch-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.receipt-claim-batch-form-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
@media (max-width: 900px) {
.receipt-claim-batch-form-page__actions {
left: 0;
}
}
</style>
+8 -3
View File
@@ -1,5 +1,6 @@
<template>
<basic-container class="receipt-claim-form-page">
<receipt-flow-batch-form v-if="batchMode" />
<basic-container v-else class="receipt-claim-form-page">
<el-form
ref="formRef"
:model="form"
@@ -182,6 +183,7 @@
import { Search } from '@element-plus/icons-vue';
import * as api from '@/api/payment/receiptFlow';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import ReceiptFlowBatchForm from './receipt-flow-batch-form.vue';
const emptyForm = () => ({
id: null,
@@ -206,7 +208,7 @@ const emptyForm = () => ({
export default {
name: 'ReceiptFlowForm',
components: { VehicleAttachmentTable },
components: { VehicleAttachmentTable, ReceiptFlowBatchForm },
data() {
return {
Search,
@@ -222,6 +224,9 @@ export default {
};
},
computed: {
batchMode() {
return String(this.$route.query.batch || '') === '1';
},
flowId() {
return this.$route.query.id || '';
},
@@ -239,7 +244,7 @@ export default {
},
},
created() {
this.initialize();
if (!this.batchMode) this.initialize();
},
methods: {
unwrapData(response) {
+44 -1
View File
@@ -60,8 +60,22 @@
>
手动同步流水
</el-button>
<el-button
v-if="hasPermission('receipt_flow_claim')"
type="primary"
:disabled="!selectedRows.length"
@click="openBatchClaim"
>
批量认领
</el-button>
</div>
<el-table v-loading="loading" :data="rows" border>
<el-table
v-loading="loading"
:data="rows"
border
@selection-change="selectedRows = $event"
>
<el-table-column type="selection" width="52" align="center" />
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
v-for="column in columns"
@@ -256,6 +270,7 @@ export default {
claimStatusOptions: api.claimStatusOptions,
columns: receiptFlowTableColumns,
rows: [],
selectedRows: [],
loading: false,
syncing: false,
searchExpanded: false,
@@ -293,6 +308,7 @@ export default {
})
);
this.rows = data.records || [];
this.selectedRows = [];
this.page.total = Number(data.total || 0);
} finally {
this.loading = false;
@@ -342,6 +358,32 @@ export default {
query: { mode: 'add', id: row.id },
});
},
openBatchClaim() {
if (!this.selectedRows.length) {
this.$message.warning('请先勾选收款流水');
return;
}
const first = this.selectedRows[0];
const payerName = String(first.payerName || '').trim();
const counterpartyAccount = String(first.counterpartyAccount || '').trim();
const sameParty = this.selectedRows.every(
row =>
String(row.payerName || '').trim() === payerName &&
String(row.counterpartyAccount || '').trim() === counterpartyAccount
);
if (!sameParty) {
this.$message.warning('批量认领所选流水的付款人、对方账号必须一致');
return;
}
this.$router.push({
path: '/payment/receipt-flow/form',
query: {
mode: 'add',
batch: '1',
ids: this.selectedRows.map(row => row.id).join(','),
},
});
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission?.[code], false);
},
@@ -398,6 +440,7 @@ export default {
display: flex;
justify-content: flex-start;
align-items: center;
gap: 8px;
padding: 12px 0;
}
.receipt-flow-page__pagination {
@@ -400,7 +400,7 @@
<div class="formal-editor__links">
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link>
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
<el-link type="danger" @click="removeInvoice($index)">删除</el-link>
<el-link v-if="editable" type="danger" @click="removeInvoice($index)">删除</el-link>
</div>
</template>
</el-table-column>
@@ -456,6 +456,9 @@
<el-table-column prop="paymentTypeName" label="付款类型" min-width="120" align="center">
<template #default="{ row }">{{ displayValue(row.paymentTypeName) }}</template>
</el-table-column>
<el-table-column label="付款方式" min-width="140" align="center">
<template #default="{ row }">{{ paymentMethodName(row.paymentMethod) }}</template>
</el-table-column>
<el-table-column prop="paymentNo" label="单据号" min-width="160" align="center" />
<el-table-column prop="appliedAmount" label="申请付款金额" min-width="145" align="center">
<template #default="{ row }">{{ formatMoney(row.appliedAmount) }}</template>
@@ -1784,6 +1787,14 @@ export default {
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
paymentMethodName(value) {
const labels = {
bank_transfer: '银行转账',
bank_draft: '银行承兑汇票',
commercial_draft: '商业承兑汇票',
};
return this.displayValue(labels[value] || value);
},
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
@@ -121,7 +121,9 @@
/>
</el-select>
<el-input-number
v-else-if="editable && column.prop === 'adjustAmount'"
v-else-if="
editable && column.prop === 'adjustAmount' && Number(row.manualFlag) === 1
"
v-model="row.adjustAmount"
:class="{
'pre-settlement-editor__negative-amount': Number(row.adjustAmount || 0) < 0,
@@ -671,6 +673,20 @@
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table-column prop="cargoName" label="货物名称" min-width="140" align="center" />
<el-table-column prop="cargoType" label="货物类型" min-width="130" align="center" />
<el-table-column label="计费规则" min-width="190" align="center">
<template #default="{ row }">
<div class="pre-settlement-editor__billing-rule-cell">
<span>{{ displayValue(billingRuleName(row)) }}</span>
<el-icon
class="pre-settlement-editor__billing-rule-icon"
title="查看计费规则详情"
@click="openBillingRuleInfo(row)"
>
<InfoFilled />
</el-icon>
</div>
</template>
</el-table-column>
<el-table-column label="运输总量" min-width="150" align="center">
<template #default="{ row }">
<span v-if="adjustDialog.readonly">{{ formatQuantity(row) }}</span>
@@ -705,18 +721,6 @@
/>
</template>
</el-table-column>
<el-table-column label="运费" min-width="140" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.freightAmount"
:min="0"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
@change="recalculateAdjustRow(row, 'freight')"
/>
</template>
</el-table-column>
<el-table-column
v-for="feeItem in adjustFeeItemNames"
:key="feeItem"
@@ -774,10 +778,76 @@
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="billingRuleDialog.visible"
title="计费规则详情"
width="720px"
append-to-body
destroy-on-close
>
<template v-if="billingRuleDialog.rule">
<div class="dialog-section-title">计费规则</div>
<el-descriptions :column="2" border class="pre-settlement-editor__billing-rule-detail">
<el-descriptions-item label="规则名称">
{{ displayValue(billingRuleDialog.rule.name) }}
</el-descriptions-item>
<el-descriptions-item label="费用类型">
{{ displayValue(billingRuleDialog.rule.feeType) }}
</el-descriptions-item>
<el-descriptions-item label="费用项">
{{ displayValue(billingRuleDialog.rule.feeItem) }}
</el-descriptions-item>
<el-descriptions-item label="计费要素">
{{ displayValue(billingRuleDialog.rule.billingElement) }}
</el-descriptions-item>
<el-descriptions-item label="计费类型">
{{ displayValue(billingRuleDialog.rule.billingType) }}
</el-descriptions-item>
<el-descriptions-item label="计费单位">
{{ displayValue(billingRuleDialog.rule.billingUnit) }}
</el-descriptions-item>
<el-descriptions-item label="单价">
{{ displayValue(billingRuleDialog.rule.unitPrice) }}
</el-descriptions-item>
<el-descriptions-item label="最低计费量">
{{ displayValue(billingRuleDialog.rule.minimumBillingWeight) }}
</el-descriptions-item>
<el-descriptions-item label="计费区间">
{{ billingRuleRanges(billingRuleDialog.rule) }}
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
{{ displayValue(billingRuleDialog.rule.remark) }}
</el-descriptions-item>
</el-descriptions>
<div class="dialog-section-title pre-settlement-editor__billing-rule-match-title">
匹配条件
</div>
<el-descriptions :column="2" border class="pre-settlement-editor__billing-rule-detail">
<el-descriptions-item label="起运地">
{{ displayValue(billingRuleDialog.rule.matchCondition.origin) }}
</el-descriptions-item>
<el-descriptions-item label="目的地">
{{ displayValue(billingRuleDialog.rule.matchCondition.destination) }}
</el-descriptions-item>
<el-descriptions-item label="运输方式">
{{ displayValue(billingRuleDialog.rule.matchCondition.transportMode) }}
</el-descriptions-item>
<el-descriptions-item label="货物类型">
{{ displayValue(billingRuleDialog.rule.matchCondition.cargoType) }}
</el-descriptions-item>
</el-descriptions>
</template>
<el-empty v-else description="暂无计费规则详情" />
<template #footer>
<el-button type="primary" @click="billingRuleDialog.visible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script>
import { mapGetters } from 'vuex';
import { InfoFilled } from '@element-plus/icons-vue';
import {
adjustDetail,
getCandidateDetails,
@@ -814,6 +884,7 @@ import * as XLSX from 'xlsx';
export default {
name: 'PreSettlementEditor',
components: { InfoFilled },
props: {
modelValue: {
type: Boolean,
@@ -835,6 +906,10 @@ export default {
type: Object,
default: null,
},
deferSave: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue', 'success'],
data() {
@@ -936,6 +1011,10 @@ export default {
reason: '',
},
adjustRows: [],
billingRuleDialog: {
visible: false,
rule: null,
},
attachmentFileTypes: [
'pdf',
'bmp',
@@ -1146,6 +1225,9 @@ export default {
this.changeRecords = [];
this.attachments = [];
this.selectedAttachmentRows = [];
this.adjustRows = [];
this.billingRuleDialog.visible = false;
this.billingRuleDialog.rule = null;
this.contractOptions = [];
this.contractDialog.visible = false;
this.contractDialog.expanded = false;
@@ -1421,6 +1503,7 @@ export default {
this.summaryFees.splice(index, 1);
},
recalculateSummaryRow(row) {
if (Number(row.manualFlag) !== 1) return;
row.settlementAmount = Number(row.originalAmount || 0) + Number(row.adjustAmount || 0);
},
buildSummaryFeesFromDetails() {
@@ -1548,6 +1631,13 @@ export default {
}
});
this.candidateDialog.confirming = true;
if (this.deferSave) {
this.buildSummaryFeesFromDetails();
this.candidateDialog.visible = false;
this.candidateDialog.confirming = false;
this.$message.success('结算明细添加成功,请点击保存提交');
return;
}
try {
const { data } = await save(this.buildSavePayload());
this.form.id = data?.data || this.form.id;
@@ -1588,6 +1678,10 @@ export default {
const isPersistedDetail =
row.id && sourceDetailId && String(row.id) !== String(sourceDetailId);
if (isPersistedDetail) return row;
if (this.deferSave) {
this.$message.warning('请先保存预结算单后再调整结算明细');
return null;
}
await this.$refs.formRef?.validate();
this.loading = true;
@@ -1621,11 +1715,97 @@ export default {
this.adjustRows = (data?.data || []).map(item => ({
...item,
feeItems: this.parseFeeItems(item.feeItemsJson),
billingRule: this.normalizeBillingRule(item),
}));
} finally {
this.adjustDialog.loading = false;
}
},
billingRuleName(row) {
return this.normalizeBillingRule(row).name;
},
openBillingRuleInfo(row) {
this.billingRuleDialog.rule = this.normalizeBillingRule(row);
this.billingRuleDialog.visible = true;
},
normalizeBillingRule(row) {
const source = row || {};
const nested = [
source.billingRule,
source.billingRuleInfo,
source.billingRuleJson,
source.billingRulesJson,
source.billingRules,
source.billingPlanRule,
source.feeRule,
source.rule,
].find(value => value !== undefined && value !== null && value !== '');
const parsedNested = this.parseJsonValue(nested);
const matchedRules = Array.isArray(parsedNested)
? parsedNested.filter(item => item && typeof item === 'object')
: [];
let rule = this.parseObject(matchedRules[0] || parsedNested || nested);
if (!Object.keys(rule).length && typeof nested === 'string') rule = { name: nested };
const matchCondition = this.parseObject(
source.matchCondition ?? source.billingMatchCondition ?? rule.matchCondition
);
const ruleNames = matchedRules
.map(item => item.ruleName || item.name || item.billingRuleName || item.feeItem)
.filter(Boolean);
const name =
source.billingRuleName ||
source.feeRuleName ||
source.ruleName ||
source.billingPlanRuleName ||
source.billingPlanName ||
rule.name ||
rule.ruleName ||
rule.billingRuleName ||
rule.planName ||
rule.billingPlanName ||
ruleNames.join('、') ||
rule.feeItem ||
'';
return {
...rule,
name,
feeType: source.billingFeeType || source.feeType || rule.feeType,
feeItem: source.billingFeeItem || source.feeItem || rule.feeItem,
billingElement:
source.billingElement ||
source.billingFactor ||
rule.billingElement ||
rule.billingFactor,
billingType: source.billingType || rule.billingType,
billingUnit: source.billingUnit || source.priceUnit || rule.billingUnit || rule.priceUnit,
unitPrice: source.billingUnitPrice ?? source.unitPrice ?? rule.unitPrice,
minimumBillingWeight:
source.minimumBillingWeight ?? rule.minimumBillingWeight ?? rule.lowerLimit,
remark: source.billingRuleRemark || source.ruleRemark || rule.remark,
rules: matchedRules,
matchCondition: {
...matchCondition,
origin: matchCondition.origin || matchCondition.originName,
destination: matchCondition.destination || matchCondition.destinationName,
transportMode: matchCondition.transportMode || matchCondition.transportType,
cargoType: matchCondition.cargoType || matchCondition.cargoTypeName,
},
};
},
billingRuleRanges(rule) {
const ranges = Array.isArray(rule?.limitRanges) ? rule.limitRanges : [];
if (ranges.length) {
return ranges
.map(
item => `${item.lowerLimit ?? '-'}~${item.upperLimit ?? '-'}${item.unitPrice ?? '-'}`
)
.join('');
}
if (rule?.lowerLimit !== undefined || rule?.upperLimit !== undefined) {
return `${rule.lowerLimit ?? '-'}~${rule.upperLimit ?? '-'}`;
}
return '-';
},
recalculateAdjustRow(row, changedField) {
if (changedField && changedField !== 'freight' && this.isFreightFeeItem(changedField)) {
row.freightAmount = Number(row.feeItems[changedField] || 0);
@@ -1797,6 +1977,25 @@ export default {
return {};
}
},
parseObject(value) {
if (!value) return {};
if (typeof value === 'object' && !Array.isArray(value)) return { ...value };
if (typeof value !== 'string') return {};
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
} catch (error) {
return {};
}
},
parseJsonValue(value) {
if (typeof value !== 'string') return value;
try {
return JSON.parse(value);
} catch (error) {
return value;
}
},
formatMoney(value, currency = 'RMB') {
if (value === undefined || value === null || value === '') return '-';
const amount = Number(value);
@@ -1983,6 +2182,28 @@ export default {
margin-top: 16px;
}
&__billing-rule-cell {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
max-width: 100%;
}
&__billing-rule-icon {
flex: 0 0 auto;
color: var(--el-color-primary);
cursor: pointer;
}
&__billing-rule-detail {
margin-top: 12px;
}
&__billing-rule-match-title {
margin-top: 20px;
}
.form-readonly {
min-height: 32px;
line-height: 32px;
@@ -6,6 +6,7 @@
page-mode
:record-id="recordId"
:initial-data="transferPayload"
:defer-save="Boolean(transferPayload)"
@success="handleSuccess"
/>
</basic-container>
@@ -699,6 +699,7 @@
append-to-body
destroy-on-close
class="settlement-detail-page__contract-dialog"
@opened="layoutGenerateContractTable"
>
<el-form
:model="generateContractDialog.query"
@@ -813,6 +814,7 @@
</el-form-item>
</el-form>
<el-table
ref="generateContractTable"
v-loading="generateContractDialog.loading"
:data="generateContractDialog.rows"
border
@@ -1454,10 +1456,16 @@ export default {
const data = this.unwrapPage(res);
this.generateContractDialog.rows = data.records || [];
this.generateContractDialog.page.total = Number(data.total || 0);
this.layoutGenerateContractTable();
} finally {
this.generateContractDialog.loading = false;
}
},
layoutGenerateContractTable() {
this.$nextTick(() => {
this.$refs.generateContractTable?.doLayout?.();
});
},
searchGenerateContracts() {
this.generateContractDialog.page.current = 1;
this.loadGenerateContracts();