Merge remote-tracking branch 'websoft/dev'

This commit is contained in:
2026-10-16 11:48:10 +08:00
10 changed files with 236 additions and 36 deletions
+2
View File
@@ -24,6 +24,8 @@ export const getContractList = (current, size, params) =>
export const getFeeOptions = () => request({ url: `${baseUrl}/fee-options`, method: 'get' });
export const getNextNo = () => request({ url: `${baseUrl}/next-no`, method: 'get' });
export const getCandidateDetails = (current, size, params) =>
request({
url: `${baseUrl}/candidate-details`,
@@ -35,8 +35,8 @@ export const sourceColumns = [
];
export const detailColumns = [
{ label: '单据号', prop: 'documentNo', minWidth: 180 },
{ label: '运单号', prop: 'waybillNo', minWidth: 160 },
{ label: '单据号', prop: 'documentNo', minWidth: 180, link: true },
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
{ label: '车号', prop: 'vehicleNo', minWidth: 130 },
{ label: '发货地址', prop: 'departureAddress', minWidth: 180 },
{ label: '到货地址', prop: 'arrivalAddress', minWidth: 180 },
+1 -1
View File
@@ -40,7 +40,7 @@ export const settlementSummaryColumns = [
export const settlementDetailColumns = [
{ label: '单据号', prop: 'documentNo', minWidth: 180, link: true },
{ label: '运单号', prop: 'waybillNo', minWidth: 160 },
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
{ label: '车号', prop: 'vehicleNo', minWidth: 130 },
{ label: '发货地址', prop: 'departureAddress', minWidth: 180 },
{ label: '到货地址', prop: 'arrivalAddress', minWidth: 180 },
+51
View File
@@ -0,0 +1,51 @@
import { getList as getReceivablePayableDetailList } from '@/api/settlement/receivable-payable-detail';
import { getList as getWaybillList } from '@/api/business/waybill-manage';
// 结算明细的「单据号」是应收应付明细单据号(应收 YS…/应付 YF…),按结算类型落到对应菜单页
const SETTLEMENT_DETAIL_PATHS = {
receivable: '/settlement/receivable-detail',
payable: '/settlement/payable-detail',
};
const unwrapPage = response => {
const body = response?.data || response || {};
return body?.data || body;
};
const firstRecordId = data => (Array.isArray(data) ? data : data?.records || [])[0]?.id || '';
export const settlementDetailPath = settlementType =>
SETTLEMENT_DETAIL_PATHS[settlementType] || SETTLEMENT_DETAIL_PATHS.payable;
/**
* 结算明细行「单据号」对应的应收应付明细ID:已落库的明细自带 sourceDetailId,未落库的行按单据号回查。
*/
export const resolveReceivablePayableDetailId = async (row = {}, settlementType) => {
if (row.sourceDetailId) return row.sourceDetailId;
if (!row.documentNo) return '';
try {
const data = unwrapPage(
await getReceivablePayableDetailList(1, 1, {
documentNo: row.documentNo,
settlementType,
})
);
return firstRecordId(data);
} catch (error) {
return '';
}
};
/**
* 结算明细行「运单号」对应的运单ID:总单来源的明细没有 waybillId(运单号列存的是总单号),按运单号回查。
*/
export const resolveWaybillId = async (row = {}) => {
if (row.waybillId) return row.waybillId;
if (!row.waybillNo) return '';
try {
const data = unwrapPage(await getWaybillList(1, 1, { waybillNo: row.waybillNo }));
return firstRecordId(data);
} catch (error) {
return '';
}
};
+29 -7
View File
@@ -79,7 +79,7 @@
<p>{{ timeRange(row) }}</p>
<p>{{ goodsSummary(row) }}</p>
<p>创建人:{{ row.createUserName || '-' }}</p>
<p>创建时间:{{ row.createTime || '-' }}</p>
<p>创建时间:{{ formatDate(row.createTime) }}</p>
</section>
<section class="route-progress">
<div class="route-track">
@@ -155,6 +155,7 @@
<script>
import * as api from '@/api/business/master-order';
import { splitRegionTokens } from '@/utils/region-address';
import TableHeaderTools from '@/components/table-header-tools/main.vue';
import MasterOrderEditor from './components/master-order-editor.vue';
import MasterOrderDispatch from './components/master-order-dispatch.vue';
@@ -332,6 +333,10 @@ export default {
? `${String(row.planStartTime).slice(0, 10)} - ${String(row.planEndTime).slice(0, 10)}`
: '-';
},
// 创建时间只展示日期。
formatDate(value) {
return value ? String(value).slice(0, 10) : '-';
},
goodsSummary(row) {
const item = (row.goods || [])[0] || {};
if (!item.cargoName && !item.cargoType) return '-';
@@ -391,6 +396,26 @@ export default {
}`;
return `${city} ${district}`;
},
// 悬停气泡里的地址:起讫点若只保存了名称或详细地址的一半,气泡就会显示成
// “柳南区”“安徽”这类残缺地址,故把两个字段互为补充后展示。
completeAddress(name, address) {
const rawName = String(name || '').trim();
const rawAddress = String(address || '').trim();
if (!rawName) return rawAddress;
if (!rawAddress) return rawName;
// 详细地址已含省市,本身就是完整地址,不再拼接名称,避免重复卡片上已有的站点名。
if (/(?:省|自治区|特别行政区|市)/.test(rawAddress)) return rawAddress;
const normalizedName = rawName.replace(/\s+/g, '');
const normalizedAddress = rawAddress.replace(/\s+/g, '');
if (normalizedAddress.includes(normalizedName)) return rawAddress;
if (normalizedName.includes(normalizedAddress)) return rawName;
// “珠海1号港口”与“珠海港口1号”这类同字异序写法只保留一个。
if ([...normalizedName].sort().join('') === [...normalizedAddress].sort().join('')) {
return normalizedName.length >= normalizedAddress.length ? rawName : rawAddress;
}
// 区域在前、详细地址在后。
return splitRegionTokens(rawName) ? `${rawName}${rawAddress}` : `${rawAddress}${rawName}`;
},
routeNodeName(value, address, transportType, region = {}) {
const text = String(value || '').trim();
if (!text) return '-';
@@ -457,15 +482,12 @@ export default {
if (this.isStationLikeName(rawAddress) && !this.isStationLikeName(rawName)) {
return {
name: rawAddress,
address: rawName || regionText || rawAddress,
address: this.completeAddress(rawName, rawAddress) || regionText || rawAddress,
};
}
const displayName = rawName || rawAddress || '-';
const tipAddress =
(rawAddress && rawAddress !== displayName ? rawAddress : '') ||
regionText ||
rawAddress ||
displayName;
this.completeAddress(rawName, rawAddress) || regionText || rawAddress || displayName;
return { name: displayName, address: tipAddress };
}
return {
@@ -475,7 +497,7 @@ export default {
siteCode,
nextTransportType,
}),
address: rawAddress || rawName || regionText || '-',
address: this.completeAddress(rawName, rawAddress) || regionText || '-',
};
},
routeNodes(row = {}) {
@@ -14,7 +14,7 @@
label-width="auto"
>
<el-row :gutter="24">
<el-col v-for="field in fields" :key="field.prop" :span="6">
<el-col v-for="field in visibleFields" :key="field.prop" :span="6">
<el-form-item :label="field.label" :prop="field.prop">
<el-date-picker
v-if="field.type === 'date' && editable"
@@ -64,6 +64,12 @@
:precision="2"
:controls="false"
/>
<span
v-else-if="field.prop === 'formalSettlementNo'"
:class="{ 'form-readonly--code': form.formalSettlementNo }"
>
{{ form.formalSettlementNo || (form.id ? '-' : '系统自动生成') }}
</span>
<span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span>
<span v-else-if="field.type === 'contract'">{{
displayValue(form.contractName)
@@ -253,7 +259,14 @@
show-overflow-tooltip
>
<template #default="{ row }">
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<el-link
v-if="column.link && row[column.prop] && !publicMode"
type="primary"
@click.stop="handleDetailLink(row, column)"
>
{{ row[column.prop] }}
</el-link>
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<span v-else-if="column.prop === 'transportQuantity'">
{{ formatDetailTransportQuantity(row[column.prop]) }}
</span>
@@ -997,6 +1010,11 @@ import {
getDetailFees as getPreSettlementDetailFees,
} from '@/api/settlement/preSettlement';
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
import {
resolveReceivablePayableDetailId,
resolveWaybillId,
settlementDetailPath,
} from '@/utils/settlement-detail-link';
import { getBfiExchangeRate } from '@/api/base/currency';
import {
createFormalSettlementForm,
@@ -1014,6 +1032,7 @@ import { h } from 'vue';
import { InfoFilled } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import { formatContractOptionLabel } from '@/utils/contract';
import { normalizeCurrencyCode } from '@/utils/currency';
import { downloadFileByUrl } from '@/utils/util';
import {
downloadKingdeeInvoiceFile,
@@ -1025,6 +1044,9 @@ import ProjectAdvanceWriteOff from './project-advance-write-off.vue';
import * as XLSX from 'xlsx';
import { isAdministrator } from '@/utils/auth';
// 结算币种为本位币(人民币)时汇率恒为1,汇率日期、结算汇率与按汇率换算的本位币合计都不展示
const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'localSettlementAmount'];
export default {
name: 'FormalSettlementEditor',
components: { InfoFilled, KingdeeInvoicePoolDialog, ProjectAdvanceWriteOff },
@@ -1195,6 +1217,14 @@ export default {
title() {
return this.readonly ? '查看正式结算单' : this.recordId ? '编辑正式结算单' : '新增正式结算单';
},
// 币种为空按人民币处理,与后端预结算/正式结算对空币种的默认取值一致
isLocalCurrencySettlement() {
return normalizeCurrencyCode(this.form.currency) === 'RMB';
},
visibleFields() {
if (!this.isLocalCurrencySettlement) return this.fields;
return this.fields.filter(field => !LOCAL_CURRENCY_HIDDEN_FIELDS.includes(field.prop));
},
summaryTotal() {
return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0);
},
@@ -2051,6 +2081,21 @@ export default {
this.appliedDetailQuery = { ...this.detailQuery };
this.detailPage.current = 1;
},
// 结算明细的「单据号」跳到该应收应付单据在应收明细/应付明细页的详情,「运单号」跳到运单详情
async handleDetailLink(row, column) {
if (column.prop === 'documentNo') {
const settlementType = row.settlementType || this.form.settlementType || 'payable';
const detailId = await resolveReceivablePayableDetailId(row, settlementType);
if (!detailId) return this.$message.info('当前记录未找到对应的应收/应付明细');
this.$router.push({ path: settlementDetailPath(settlementType), query: { detailId } });
return;
}
if (column.prop === 'waybillNo') {
const waybillId = await resolveWaybillId(row);
if (!waybillId) return this.$message.info('当前记录未找到对应的运单详情');
this.$router.push({ path: '/business/waybill-manage/detail', query: { id: waybillId } });
}
},
resetDetailQuery(apply = true) {
this.detailQuery = { documentNo: '', waybillNo: '', batchNo: '', cargoName: '' };
if (apply) this.handleDetailQuery();
@@ -2891,6 +2936,9 @@ export default {
</script>
<style scoped lang="scss">
.form-readonly--code {
color: var(--el-color-primary);
}
.formal-editor--page {
padding-bottom: 72px;
}
@@ -16,7 +16,7 @@
class="pre-settlement-editor__form"
>
<el-row :gutter="24">
<el-col v-for="field in formFields" :key="field.prop" :span="field.span || 6">
<el-col v-for="field in visibleFormFields" :key="field.prop" :span="field.span || 6">
<el-form-item :label="field.label" :prop="field.prop">
<el-input
v-if="field.type === 'contract'"
@@ -40,7 +40,7 @@
type="date"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
:disabled="!editable || form.currency === 'RMB'"
:disabled="!editable"
placeholder="请选择"
@change="handleExchangeRateDateChange"
/>
@@ -50,7 +50,7 @@
:min="0"
:precision="2"
:controls="false"
:disabled="!editable || form.currency === 'RMB'"
:disabled="!editable"
@change="recalculateLocalAmount"
/>
<el-input
@@ -66,7 +66,11 @@
<span v-else-if="field.prop === 'settlementType'" class="form-readonly">
{{ settlementTypeName }}
</span>
<span v-else-if="field.prop === 'preSettlementNo'" class="form-readonly">
<span
v-else-if="field.prop === 'preSettlementNo'"
class="form-readonly"
:class="{ 'form-readonly--code': form.preSettlementNo }"
>
{{ form.preSettlementNo || (!form.id ? '系统自动生成' : '-') }}
</span>
<span v-else-if="field.prop === 'createTime'" class="form-readonly">
@@ -253,9 +257,9 @@
>
<template #default="{ row }">
<el-link
v-if="column.link && row[column.prop]"
v-if="column.link && row[column.prop] && !publicMode"
type="primary"
@click="openAdjustDialog(row, true)"
@click.stop="handleDetailLink(row, column)"
>
{{ row[column.prop] }}
</el-link>
@@ -955,14 +959,21 @@ import {
adjustDetail,
getCandidateDetails,
getContractList,
getContractOptions,
getDetail,
getDetailFees,
getFeeOptions,
getNextNo,
removeDetail,
save,
submit,
} from '@/api/settlement/preSettlement';
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
import {
resolveReceivablePayableDetailId,
resolveWaybillId,
settlementDetailPath,
} from '@/utils/settlement-detail-link';
import { getBfiExchangeRate } from '@/api/base/currency';
import { getDictionary } from '@/api/system/dictbiz';
import {
@@ -985,11 +996,15 @@ import {
settlementSummaryColumns,
} from '@/option/settlement/preSettlementTable';
import { formatContractOptionLabel } from '@/utils/contract';
import { normalizeCurrencyCode } from '@/utils/currency';
import { downloadFileByUrl } from '@/utils/util';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import * as XLSX from 'xlsx';
import { isAdministrator } from '@/utils/auth';
// 结算币种为本位币(人民币)时汇率恒为1,汇率日期、结算汇率与按汇率换算的本位币合计都不展示
const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'localSettlementAmount'];
export default {
name: 'PreSettlementEditor',
components: { InfoFilled, ChangeRecordDetailDialog, ProjectAdvanceWriteOff },
@@ -1200,6 +1215,14 @@ export default {
settlementTypeName() {
return this.form.settlementType === 'receivable' ? '应收' : '应付';
},
// 币种为空按人民币处理,与后端 normalizeRate 的取默认值一致
isLocalCurrencySettlement() {
return normalizeCurrencyCode(this.form.currency) === 'RMB';
},
visibleFormFields() {
if (!this.isLocalCurrencySettlement) return this.formFields;
return this.formFields.filter(field => !LOCAL_CURRENCY_HIDDEN_FIELDS.includes(field.prop));
},
contractDialogCategory() {
return '承运商合同';
},
@@ -1295,6 +1318,11 @@ export default {
},
async initialize() {
this.resetEditor();
// 新增页在保存前没有创建人/创建日期,先按当前登录用户与当前时间展示将要落库的值
if (!this.recordId) {
this.form.createUserName = this.userInfo?.realName || this.userInfo?.userName || '';
this.form.createTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
}
if (!this.publicMode) {
await this.loadFeeOptions();
await this.loadFeeCategoryOptions();
@@ -1305,15 +1333,36 @@ export default {
else if (this.initialData) {
await this.applyInitialData();
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
await this.refreshPreSettlementNo();
await this.fetchBfiExchangeRate();
} else if (!this.publicMode) {
await this.refreshPreSettlementNo();
}
},
async refreshPreSettlementNo() {
if (this.form.id) return;
const response = await getNextNo();
this.form.preSettlementNo = response.data?.data || '';
},
async applyInitialData() {
const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : [];
if (!rows.length) return;
const first = rows[0];
const settlementType = this.initialData.settlementType || first.settlementType || 'payable';
const contractId = first.contractId || '';
let contractPartyA = first.payerName || '';
let contractPartyB = first.payeeName || '';
if (contractId && (!contractPartyA || !contractPartyB)) {
try {
const { data } = await getContractOptions(first.contractNo || '');
const matched = (data?.data || []).find(item => String(item.id) === String(contractId));
contractPartyA = contractPartyA || matched?.partyA || '';
contractPartyB = contractPartyB || matched?.partyB || '';
} catch (error) {
contractPartyA = '';
contractPartyB = '';
}
}
if (!this.contractOptions.some(item => String(item.id) === String(contractId))) {
this.contractOptions.push({
id: contractId,
@@ -1324,8 +1373,8 @@ export default {
deptId: first.deptId,
deptName: first.deptName,
settlementType,
partyA: first.payerName,
partyB: first.payeeName,
partyA: contractPartyA,
partyB: contractPartyB,
});
}
this.handleContractChange(contractId);
@@ -1337,8 +1386,8 @@ export default {
projectName: first.projectName || this.form.projectName,
deptId: first.deptId || this.form.deptId,
deptName: first.deptName || this.form.deptName,
payerName: first.payerName || this.form.payerName,
payeeName: first.payeeName || this.form.payeeName,
payerName: this.form.payerName || contractPartyA,
payeeName: this.form.payeeName || contractPartyB,
settlementType,
currency: first.currency || 'RMB',
localCurrency: first.localCurrency || 'RMB',
@@ -1610,8 +1659,9 @@ export default {
this.form.deptId = contract.deptId;
this.form.deptName = contract.deptName;
this.form.settlementType = contract.settlementType || 'payable';
this.form.payerName = contract.partyA;
this.form.payeeName = contract.partyB;
// 部分候选合同只带 id/名称,缺甲方乙方;此时保留已带出的收付方,不要清成 undefined
if (contract.partyA) this.form.payerName = contract.partyA;
if (contract.partyB) this.form.payeeName = contract.partyB;
this.form.currency = contract.settlementCurrency || 'RMB';
this.summaryFees = [];
this.projectAdvanceWriteOffs = [];
@@ -2014,6 +2064,21 @@ export default {
this.appliedDetailQuery = { ...this.detailQuery };
this.detailPage.current = 1;
},
// 结算明细的「单据号」跳到该应收应付单据在应收明细/应付明细页的详情,「运单号」跳到运单详情
async handleDetailLink(row, column) {
if (column.prop === 'documentNo') {
const settlementType = row.settlementType || this.form.settlementType || 'payable';
const detailId = await resolveReceivablePayableDetailId(row, settlementType);
if (!detailId) return this.$message.info('当前记录未找到对应的应收/应付明细');
this.$router.push({ path: settlementDetailPath(settlementType), query: { detailId } });
return;
}
if (column.prop === 'waybillNo') {
const waybillId = await resolveWaybillId(row);
if (!waybillId) return this.$message.info('当前记录未找到对应的运单详情');
this.$router.push({ path: '/business/waybill-manage/detail', query: { id: waybillId } });
}
},
resetDetailQuery() {
this.detailQuery = {
documentNo: '',
@@ -3019,6 +3084,10 @@ export default {
line-height: 32px;
color: #303133;
}
.form-readonly--code {
color: var(--el-color-primary);
}
}
.dialog-section-title {
@@ -520,7 +520,7 @@
:index="transferIndexMethod"
/>
<el-table-column
v-for="column in tableColumns.slice(0, 13)"
v-for="column in transferTableColumns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
@@ -1174,6 +1174,13 @@ export default {
columns.splice(totalIndex, 0, ...dynamicColumns);
return columns;
},
// 批量转结算弹窗只展示前 13 列。预结算单号、正式结算单号正是本操作要生成的单号,
// 候选行上必为空,故不占列
transferTableColumns() {
return this.tableColumns
.filter(column => !['preSettlementNo', 'formalSettlementNo'].includes(column.prop))
.slice(0, 13);
},
columnSettingOptions() {
return this.displayTableColumns.map(column => ({
prop: column.prop,
@@ -1712,11 +1719,12 @@ export default {
this.$router.push({ path, query: { detailNo } });
},
async openRouteDetail(detailId = this.$route.query.detailId) {
if (
!detailId ||
this.settlementType !== 'payable' ||
this.$route.path !== '/settlement/payable-detail'
) {
// 结算明细点「单据号」会带 detailId 跳到本页;只有当前停在应收/应付明细页时才自动展开详情
const routePath =
this.settlementType === 'payable'
? '/settlement/payable-detail'
: '/settlement/receivable-detail';
if (!detailId || this.$route.path !== routePath) {
return;
}
let row = this.rows.find(item => String(item.id) === String(detailId));
@@ -1729,7 +1737,9 @@ export default {
if (data.records?.[0]) row = this.decorateRow(data.records[0]);
}
if (!row) {
this.$message.info('当前记录未找到对应的应付明细');
this.$message.info(
`当前记录未找到对应的${this.settlementType === 'payable' ? '应付' : '应收'}明细`
);
return;
}
await this.openDetailDialog(row);
+2 -4
View File
@@ -371,9 +371,8 @@ export default {
placeholder: '日期时间选择器',
},
{
label: '里程/航程数(公里)',
label: '里程/航程数',
prop: 'mileage',
renderHeader: () => '里程/航程数',
type: 'input',
minWidth: 130,
slot: true,
@@ -391,6 +390,7 @@ export default {
],
value: '公里',
hide: true,
display: false,
},
{
label: '备注',
@@ -565,12 +565,10 @@ export default {
const mileageUnit = vehicleType === '船舶' ? '海里' : '公里';
const vehicleTypeColumn = this.findColumn(this.option.column, 'vehicleType');
const vehicleNoColumn = this.findColumn(this.option.column, 'vehicleNo');
const mileageColumn = this.findColumn(this.option.column, 'mileage');
const mileageUnitColumn = this.findColumn(this.option.column, 'mileageUnit');
vehicleNoColumn.placeholder =
vehicleType === '船舶' ? '请选择船号' : '请选择车牌号';
vehicleTypeColumn.disabled = Boolean(this.form.id);
mileageColumn.label = `里程/航程数(${mileageUnit})`;
mileageUnitColumn.value = mileageUnit;
this.form.mileageUnit = mileageUnit;
if (!this.isDetailLoading && oldValue && vehicleType !== oldValue) {