1、调整导入运单

2、调整对账
This commit is contained in:
2026-09-10 22:50:18 +08:00
parent 38e53e01af
commit 1a9cd55d66
17 changed files with 374 additions and 149 deletions
+1
View File
@@ -36,6 +36,7 @@ export const roadLoading = ids =>
});
export const getImportBatches = params => request({ url: `${baseUrl}/import-batch/list`, method: 'get', params });
export const getImportBatchNextCode = () => request({ url: `${baseUrl}/import-batch/next-code`, method: 'get' });
export const getImportDetails = params => request({ url: `${baseUrl}/import-batch/details`, method: 'get', params });
export const removeImportBatches = ids => request({ url: `${baseUrl}/import-batch/remove`, method: 'post', params: { ids } });
export const getImportOptions = () => request({ url: `${baseUrl}/import-batch/options`, method: 'get' });
@@ -29,8 +29,8 @@ export const complete = id =>
request({ url: `${baseUrl}/complete`, method: 'post', params: { id } });
export const completeWithData = data =>
request({ url: `${baseUrl}/complete-with-data`, method: 'post', data });
export const template = mode =>
request({ url: `${baseUrl}/template`, method: 'get', params: { mode }, responseType: 'blob' });
export const template = (mode, params) =>
request({ url: `${baseUrl}/template`, method: 'get', params: { mode, ...params }, responseType: 'blob' });
const importFile = (url, id, file) => {
const data = new FormData();
+5
View File
@@ -97,6 +97,11 @@ export const businessTypeOptions = [
{ label: '普通业务', value: '普通业务' },
];
export const businessModeOptions = [
{ label: '国内运输', value: '国内运输' },
{ label: '跨境运输', value: '跨境运输' },
];
export const settlementModeOptions = [
{ label: '先票后款', value: '先票后款' },
{ label: '先款后票', value: '先款后票' },
+20 -1
View File
@@ -1,5 +1,6 @@
import {
auditColumns,
businessModeOptions,
businessTypeOptions,
createCrudOption,
nonNegativeRule,
@@ -129,7 +130,7 @@ export const option = createCrudOption([
hide: true,
minWidth: 140,
maxlength: 20,
rules: textRule('项目简称', 20, true),
rules: textRule('项目简称', 20),
},
{
label: '项目类型',
@@ -305,6 +306,15 @@ export const option = createCrudOption([
hide: true,
minWidth: 130,
},
{
label: '业务模式',
prop: 'businessMode',
type: 'select',
dicData: businessModeOptions,
hide: true,
minWidth: 120,
rules: [selectRule('业务模式')],
},
{
label: '项目规模(万元)',
prop: 'projectScale',
@@ -323,6 +333,15 @@ export const option = createCrudOption([
minWidth: 150,
rules: [nonNegativeRule('预计利润')],
},
{
label: '利润率(%',
prop: 'profitRate',
type: 'number',
precision: 2,
hide: true,
minWidth: 120,
rules: [nonNegativeRule('利润率')],
},
{
label: '资金需求(万元)',
prop: 'fundDemand',
@@ -67,9 +67,7 @@ export const feeDetailBaseColumns = [
];
export const feeDetailTailColumns = [
{ label: '原总金额', prop: 'originalAmountText', minWidth: 120 },
{ label: '调整金额', prop: 'adjustAmountText', minWidth: 120 },
{ label: '调整后总金额', prop: 'afterAmountText', minWidth: 140 },
{ label: '结算金额', prop: 'originalAmountText', minWidth: 120 },
{ label: '备注', prop: 'remark', minWidth: 160 },
{ label: '最后录入人', prop: 'updateUserName', minWidth: 120 },
{ label: '最后录入时间', prop: 'updateTime', minWidth: 170 },
+5
View File
@@ -76,6 +76,11 @@ export const option = {
prop: 'posts',
minWidth: 150,
},
{
label: '驾驶车辆',
prop: 'drivingVehicle',
minWidth: 130,
},
{
label: '所属组织',
prop: 'organizationName',
@@ -21,7 +21,7 @@ export const option = {
label: '车牌号',
prop: 'plateNo',
search: true,
searchOrder: 6,
searchOrder: 7,
slot: true,
minWidth: 120,
},
@@ -29,7 +29,7 @@ export const option = {
label: '业务关系',
prop: 'businessRelation',
search: true,
searchOrder: 5,
searchOrder: 6,
searchValue: '',
type: 'select',
dicData: [
@@ -50,11 +50,18 @@ export const option = {
slot: true,
minWidth: 150,
},
{
label: '使用部门',
prop: 'useDepartment',
search: true,
searchOrder: 3,
minWidth: 140,
},
{
label: '车辆类型',
prop: 'vehicleType',
search: true,
searchOrder: 4,
searchOrder: 5,
type: 'select',
dicData: [
{ label: '全部', value: '' },
@@ -108,7 +115,7 @@ export const option = {
label: '车辆状态',
prop: 'status',
search: true,
searchOrder: 3,
searchOrder: 4,
type: 'select',
slot: true,
dicData: [
@@ -655,23 +655,10 @@ const extractRecords = res => {
if (Array.isArray(data?.data?.records)) return data.data.records;
return [];
};
// 批次号规则:PC + 导入日期 + 当日批次流水号(4 位),如 PC202606010001
const batchNoPrefix = 'PC';
const batchNoSequenceLength = 4;
// 批次号由后端按 PC + 导入日期 + 当日流水生成,已删除数据也计入流水,避免唯一索引冲突
const generateBatchNo = async () => {
const prefix = `${batchNoPrefix}${dayjs().format('YYYYMMDD')}`;
let maxSequence = 0;
try {
const res = await api.getImportBatches({ current: 1, size: 9999 });
extractRecords(res).forEach(item => {
const no = String(item.batchNo || '').trim();
if (!no.startsWith(prefix)) return;
maxSequence = Math.max(maxSequence, Number(no.slice(prefix.length)) || 0);
});
} catch (error) {
maxSequence = 0;
}
return `${prefix}${String(maxSequence + 1).padStart(batchNoSequenceLength, '0')}`;
const res = await api.getImportBatchNextCode();
return res?.data?.data || '';
};
// 承运商合同的承运商固定取合同乙方,与运单管理(waybill-manage-page)保持一致。
const getCarrierContractPartyName = (contract = {}) =>
+28 -3
View File
@@ -92,7 +92,7 @@
<section class="change-section attachment-section">
<div class="section-head"><div class="dialog-section-title">其它附件</div><el-button type="primary" plain>批量下载</el-button></div>
<el-table :data="attachments" border class="change-table"><el-table-column type="index" label="序号" width="70" /><el-table-column label="文件名" min-width="240"><template #default="{ row }"><el-link type="primary" @click="previewAttachment(row, attachments)">{{ row.originalName || row.name }}</el-link></template></el-table-column><el-table-column prop="description" label="附件描述" min-width="240" /><el-table-column prop="size" label="文件大小" width="120" /><el-table-column prop="userName" label="上传人" width="140" /><el-table-column prop="uploadTime" label="上传时间" width="180" /><el-table-column label="操作" width="100"><template #default="{ $index }"><el-link type="danger" @click="attachments.splice($index, 1)">删除</el-link></template></el-table-column></el-table>
<el-table :data="attachments" border class="change-table"><el-table-column type="index" label="序号" width="70" /><el-table-column label="文件名" min-width="240"><template #default="{ row }"><el-link type="primary" @click="previewAttachment(row, attachments)">{{ row.originalName || row.name }}</el-link></template></el-table-column><el-table-column prop="description" label="附件描述" min-width="240" /><el-table-column prop="size" label="文件大小" width="120" /><el-table-column prop="uploadUserName" label="上传人" width="140" /><el-table-column prop="uploadTime" label="上传时间" width="180" /><el-table-column label="操作" width="100"><template #default="{ $index }"><el-link type="danger" @click="attachments.splice($index, 1)">删除</el-link></template></el-table-column></el-table>
<el-upload action="#" :auto-upload="false" multiple :show-file-list="false" @change="handleAttachment"><el-button type="primary" plain icon="el-icon-upload">上传附件</el-button></el-upload>
</section>
@@ -171,8 +171,33 @@ export default {
savePlan(value, index) { if (index < 0) this.plans.push(value); else this.plans.splice(index, 1, value); if (value.defaultPlan) this.plans.forEach((item, current) => { if (current !== (index < 0 ? this.plans.length - 1 : index)) item.defaultPlan = false; }); },
handleSettlementTypeChange(value) { if (value !== '月结') { this.settlementRule.billCycleType = ''; this.settlementRule.billCutoffDay = ''; } else if (!this.settlementRule.billCycleType) this.settlementRule.billCycleType = '固定截单日'; if (value !== '固定天数周期结算') this.settlementRule.cycleDays = ''; },
handleCycleTypeChange(value) { if (value === '固定截单日' && !this.settlementRule.billCutoffDay) this.settlementRule.billCutoffDay = 25; if (value !== '固定截单日') this.settlementRule.billCutoffDay = ''; },
handleAttachment(event) { const raw = event.raw; if (!raw) return; if (raw.size > 50 * 1024 * 1024) { this.$message.warning('单个文件大小不能超过50M'); return; } this.attachments.push({ name: raw.name, size: `${Math.ceil(raw.size / 1024)}KB`, uploadTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss') }); },
handleContractFileChange(list) { this.contractFileRows = (list || []).map(item => ({ ...item, uploadTime: item.uploadTime || this.$dayjs().format('YYYY-MM-DD HH:mm:ss') })); },
currentUploadUserName() {
const userInfo = this.$store.getters.userInfo || {};
return userInfo.realName || userInfo.userName || '';
},
handleAttachment(event) {
const raw = event.raw;
if (!raw) return;
if (raw.size > 50 * 1024 * 1024) {
this.$message.warning('单个文件大小不能超过50M');
return;
}
this.attachments.push({
name: raw.name,
size: `${Math.ceil(raw.size / 1024)}KB`,
uploadUserName: this.currentUploadUserName(),
uploadTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
});
},
handleContractFileChange(list) {
const uploadUserName = this.currentUploadUserName();
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.contractFileRows = (list || []).map(item => ({
...item,
uploadUserName: item.uploadUserName || uploadUserName,
uploadTime: item.uploadTime || uploadTime,
}));
},
attachmentUrl(row = {}) { return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || ''; },
attachmentName(row = {}) { return row.originalName || row.name || row.fileName || '附件'; },
attachmentExtension(row = {}) { const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0]; const index = source.lastIndexOf('.'); return index > -1 ? source.slice(index + 1).toLowerCase() : ''; },
+28 -16
View File
@@ -384,6 +384,16 @@
/>
</el-select>
</el-form-item>
<el-form-item label="业务模式" prop="businessMode">
<el-select v-model="form.businessMode" placeholder="请选择业务模式">
<el-option
v-for="item in businessModeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="项目规模" prop="projectScale">
<el-input
v-model="form.projectScale"
@@ -410,6 +420,14 @@
<template #suffix>万元</template>
</el-input>
</el-form-item>
<el-form-item label="利润率" prop="profitRate">
<el-input
v-model="form.profitRate"
@input="value => handleNumberInput('profitRate', value)"
>
<template #suffix>%</template>
</el-input>
</el-form-item>
<el-form-item label="履约保证金" prop="fundDemand">
<el-input
v-model="form.fundDemand"
@@ -670,17 +688,6 @@
</el-table>
<template v-if="isChangeDialog">
<div class="dialog-section-title">变更内容</div>
<el-form-item prop="changeContent" class="project-apply-form__change-textarea">
<el-input
v-model="form.changeContent"
type="textarea"
:rows="2"
placeholder="2000字以内"
maxlength="2000"
show-word-limit
/>
</el-form-item>
<div class="dialog-section-title">变更原因</div>
<el-form-item prop="changeReason" class="project-apply-form__change-textarea">
<el-input
@@ -852,6 +859,7 @@ import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import {
businessModeOptions,
businessTypeOptions,
projectSourceOptions,
projectTypeOptions,
@@ -886,9 +894,11 @@ const emptyForm = () => ({
transportRoute: '',
transportType: '',
businessType: '',
businessMode: '国内运输',
projectScale: '',
settlementMode: '',
estimatedProfit: '',
profitRate: '',
fundDemand: '',
handlerUserId: '',
handlerUserName: '',
@@ -909,6 +919,7 @@ const emptyForm = () => ({
const optionalSentinelFields = [
'projectScale',
'estimatedProfit',
'profitRate',
'fundDemand',
'receivableDays',
'paymentDays',
@@ -1001,6 +1012,7 @@ export default {
projectSourceOptions,
transportTypeOptions: [],
businessTypeOptions,
businessModeOptions,
settlementModeOptions: [],
changeTypeOptions,
tableForm: {},
@@ -1035,11 +1047,11 @@ export default {
{ max: 50, message: '项目名称不能超过50个字符', trigger: 'blur' },
],
projectShortName: [
{ required: true, message: '请输入项目简称', trigger: 'blur' },
{ max: 20, message: '项目简称不能超过20个字符', trigger: 'blur' },
],
businessDeptId: [{ required: true, message: '请选择业务部门', trigger: 'change' }],
undertakeDeptId: [{ required: true, message: '请选择承办部门', trigger: 'change' }],
businessMode: [{ required: true, message: '请选择业务模式', trigger: 'change' }],
customerNames: [{ required: true, message: '请选择客户名称', trigger: 'change' }],
carrierNames: [{ required: true, message: '请选择下游承运商', trigger: 'change' }],
fundLimit: [
@@ -1066,6 +1078,7 @@ export default {
],
projectScale: [{ label: '项目规模', validator: validateAmount, trigger: 'blur' }],
estimatedProfit: [{ label: '预估利润', validator: validateAmount, trigger: 'blur' }],
profitRate: [{ label: '利润率', validator: validateAmount, trigger: 'blur' }],
fundDemand: [{ label: '履约保证金', validator: validateAmount, trigger: 'blur' }],
handlerUserId: [
{
@@ -1087,9 +1100,6 @@ export default {
trigger: 'change',
},
],
changeContent: [
{ max: 2000, message: '变更内容不能超过2000个字符', trigger: 'blur' },
],
changeReason: [
{ max: 2000, message: '变更原因不能超过2000个字符', trigger: 'blur' },
],
@@ -1570,6 +1580,7 @@ export default {
receivableLimit: this.formatAmount(row.receivableLimit),
projectScale: this.normalizeOptionalSentinel(row.projectScale),
estimatedProfit: this.normalizeOptionalSentinel(row.estimatedProfit),
profitRate: this.normalizeOptionalSentinel(row.profitRate),
fundDemand: this.normalizeOptionalSentinel(row.fundDemand),
receivableDays: this.normalizeOptionalSentinel(row.receivableDays),
paymentDays: this.normalizeOptionalSentinel(row.paymentDays),
@@ -1760,8 +1771,9 @@ export default {
if (!options.includeChangeType) {
delete submitRow.changeType;
}
// 变更内容字段已下线,提交时始终清空,避免沿用历史值
submitRow.changeContent = '';
if (this.isNewProjectDialog && !options.includeChangeType) {
delete submitRow.changeContent;
delete submitRow.changeReason;
}
['projectIntro', 'profitRemark', 'riskPoint', 'emergencyPlan'].forEach(
+7 -5
View File
@@ -279,7 +279,7 @@
class="voucher-manage-page__upload-progress"
/></el-form-item>
<el-form-item label="关联运输批次" prop="waybillImportBatchIds"
><el-button type="primary" @click="openBatchDialog">选择</el-button></el-form-item
><el-button type="primary" @click="openBatchDialog()">选择</el-button></el-form-item
>
</el-form>
</section-card>
@@ -886,13 +886,15 @@ watch(uploadVisible, visible => {
if (!visible && !closingUploadDialog.value) void stopCurrentUpload(editing.fileTaskId);
});
const openBatchDialog = async row => {
batchDialogMode.value = row ? 'change' : 'upload';
batchTarget.value = row;
// 按钮 @click 会传入 MouseEvent,不能把事件对象当成凭证行。
const voucher = row instanceof Event ? undefined : row;
batchDialogMode.value = voucher?.id ? 'change' : 'upload';
batchTarget.value = voucher?.id ? voucher : undefined;
batchSelection.value = [];
batchVisible.value = true;
await loadBatches();
if (row?.waybillBatchNo) {
const currentBatch = batchRows.value.find(item => item.batchNo === row.waybillBatchNo);
if (voucher?.waybillBatchNo) {
const currentBatch = batchRows.value.find(item => item.batchNo === voucher.waybillBatchNo);
if (currentBatch) batchSelection.value = [currentBatch];
}
};
@@ -565,7 +565,7 @@
/>
</el-form-item>
<el-form-item label="合同类别">
<el-select v-model="contractDialog.query.contractCategory" clearable placeholder="请选择">
<el-select :model-value="contractDialogCategory" disabled placeholder="请选择">
<el-option
v-for="item in contractCategoryOptions"
:key="item.value"
@@ -1027,7 +1027,7 @@ export default {
contractName: '',
projectName: '',
organizationName: '',
contractCategory: '',
contractCategory: '承运商合同',
signType: '',
effectiveType: '',
contractStage: '',
@@ -1161,6 +1161,9 @@ export default {
settlementTypeName() {
return this.form.settlementType === 'receivable' ? '应收' : '应付';
},
contractDialogCategory() {
return '承运商合同';
},
summaryTotal() {
// 与结算合计表格的合计行保持一致:包含手工添加的费用与明细调整后的金额。
if (this.summaryFees.length) {
@@ -1257,8 +1260,8 @@ export default {
deptId: first.deptId,
deptName: first.deptName,
settlementType,
partyA: settlementType === 'receivable' ? first.payeeName : first.payerName,
partyB: settlementType === 'receivable' ? first.payerName : first.payeeName,
partyA: first.payerName,
partyB: first.payeeName,
});
}
this.handleContractChange(contractId);
@@ -1415,7 +1418,10 @@ export default {
const { data } = await getContractList(
this.contractDialog.page.current,
this.contractDialog.page.size,
{ ...this.contractDialog.query }
{
...this.contractDialog.query,
contractCategory: this.contractDialogCategory,
}
);
const page = data?.data || {};
this.contractDialog.rows = page.records || [];
@@ -1434,7 +1440,7 @@ export default {
contractName: '',
projectName: '',
organizationName: '',
contractCategory: '',
contractCategory: this.contractDialogCategory,
signType: '',
effectiveType: '',
contractStage: '',
@@ -1519,13 +1525,8 @@ export default {
this.form.deptId = contract.deptId;
this.form.deptName = contract.deptName;
this.form.settlementType = contract.settlementType || 'payable';
if (this.form.settlementType === 'receivable') {
this.form.payerName = contract.partyB;
this.form.payeeName = contract.partyA;
} else {
this.form.payerName = contract.partyA;
this.form.payeeName = contract.partyB;
}
this.form.payerName = contract.partyA;
this.form.payeeName = contract.partyB;
this.summaryFees = [];
},
async saveDraft(shouldSubmit) {
@@ -205,10 +205,18 @@
<section-card title="导入外部账单">
<template #extra>
<div class="reconciliation-editor__actions" v-if="editable">
<el-button type="primary" plain @click="downloadTemplate('vehicle')"
<el-button
v-if="form.reconciliationMode === 'vehicle'"
type="primary"
plain
@click="downloadTemplate('vehicle')"
>下载整车对账模板</el-button
>
<el-button type="primary" plain @click="downloadTemplate('cargo')"
<el-button
v-if="form.reconciliationMode === 'cargo'"
type="primary"
plain
@click="downloadTemplate('cargo')"
>下载货物明细对账模板</el-button
>
<el-button type="primary" plain @click="chooseImport">导入</el-button>
@@ -374,6 +382,11 @@
formatMoney(row.settlementAmount, row.currency)
}}</template></el-table-column
>
<el-table-column label="审批状态" width="110"
><template #default="{ row }">{{
formalApprovalStatusLabel(row.approvalStatus)
}}</template></el-table-column
>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click="selectFormal(row)">选择</el-link>
@@ -1081,6 +1094,19 @@ export default {
getFeeItemAmount(row, name) {
return (row.feeItems || this.parseFeeItems(row.feeItemsJson))[name] ?? 0;
},
formalApprovalStatusLabel(value) {
return (
{
draft: '草稿',
reviewing: '审批中',
approved: '审批通过',
returned: '已驳回',
voided: '已作废',
}[value] ||
value ||
'-'
);
},
async loadFormalOptions() {
this.formalDialog.loading = true;
try {
@@ -1096,20 +1122,8 @@ export default {
params
)
);
let rows = data.records || [];
let total = data.total || 0;
if (!rows.length) {
const fallback = await formalSettlementApi.getList(
this.formalDialog.page.current,
this.formalDialog.page.size,
params
);
const fallbackData = this.unwrapData(fallback);
rows = fallbackData.records || [];
total = fallbackData.total || 0;
}
this.formalDialog.rows = rows;
this.formalDialog.page.total = total;
this.formalDialog.rows = data.records || [];
this.formalDialog.page.total = data.total || 0;
} finally {
this.formalDialog.loading = false;
}
@@ -1120,6 +1134,9 @@ export default {
await this.selectFormal(this.formalDialog.selected[0]);
},
async selectFormal(selected) {
if (selected?.approvalStatus && selected.approvalStatus !== 'draft') {
return this.$message.warning('只能选择草稿状态的正式结算单');
}
const formalSettlementChanged = this.form.formalSettlementId !== selected.id;
this.form = {
...this.form,
@@ -1489,7 +1506,14 @@ export default {
}
},
async downloadTemplate(mode) {
const response = await api.template(mode);
if (!this.form.formalSettlementId) {
return this.$message.warning('请先选择正式结算单');
}
const response = await api.template(mode, {
id: this.currentId,
formalSettlementId: this.form.formalSettlementId,
feeItems: this.internalFeeItemNames.join(','),
});
downloadXls(
response.data,
`${mode === 'cargo' ? '货物明细对账模板' : '整车总额对账模板'}.xlsx`
@@ -145,7 +145,9 @@
:align="column.align || 'center'"
show-overflow-tooltip
>
<template #default="{ row }">{{ formatDetailCell(row, column.prop) }}</template>
<template #default="{ row }">
{{ formatFeeDetailCell(row, column.prop) }}
</template>
</el-table-column>
</el-table>
</el-tab-pane>
@@ -398,12 +400,14 @@
maxlength="300"
placeholder="请输入调整原因"
/>
<span v-else-if="column.prop === 'adjustAmountText'">
{{ fixedTwoDecimals(row.adjustAmount) }}
</span>
<span v-else-if="column.prop === 'afterAmountText'">
{{ fixedTwoDecimals(row.afterAmount) }}
<span v-else-if="column.prop === 'originalAmountText'">
{{
Number(row.adjustAmount || 0) !== 0
? fixedTwoDecimals(row.afterAmount)
: fixedTwoDecimals(row.originalAmount)
}}
</span>
<span v-else>{{ formatDetailCell(row, column.prop) }}</span>
</template>
</el-table-column>
@@ -1135,7 +1139,13 @@ export default {
},
feeDetailColumns() {
const baseColumns = feeDetailBaseColumns.filter(column => column.prop !== 'freightAmount');
return [...baseColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
const hasAdjustment = this.feeRows.some(row => Number(row.adjustAmount || 0) !== 0);
const tailColumns = feeDetailTailColumns.map(col =>
col.prop === 'originalAmountText' && hasAdjustment
? { ...col, label: '调整后金额' }
: col
);
return [...baseColumns, ...this.dynamicFeeColumns, ...tailColumns];
},
displayTableColumns() {
const columns = this.tableColumns.filter(
@@ -1181,7 +1191,7 @@ export default {
adjustFeeColumns() {
const baseColumns = feeDetailBaseColumns.filter(column => column.prop !== 'freightAmount');
const tailColumns = feeDetailTailColumns.filter(column => column.prop !== 'remark');
const afterAmountIndex = tailColumns.findIndex(column => column.prop === 'afterAmountText');
const afterAmountIndex = tailColumns.findIndex(column => column.prop === 'originalAmountText');
tailColumns.splice(afterAmountIndex + 1, 0, {
label: '调整原因',
prop: 'changeReason',
@@ -2596,6 +2606,22 @@ export default {
}
return this.formatCell(row?.[prop]);
},
formatFeeDetailCell(row, prop) {
if (prop !== 'originalAmountText') return this.formatDetailCell(row, prop);
const adjustAmount = Number(row?.adjustAmount || 0);
if (adjustAmount === 0) return this.formatDetailCell(row, prop);
const adjustedAmount = Number(
row?.afterAmount ?? row?.adjustedAmount ?? row?.settlementAmount
);
if (Number.isFinite(adjustedAmount)) return this.fixedTwoDecimals(adjustedAmount);
const originalAmount = Number(row?.originalAmount ?? row?.originalAmountText);
return Number.isFinite(originalAmount)
? this.fixedTwoDecimals(originalAmount + adjustAmount)
: this.formatCell(row?.afterAmountText);
},
formatCell(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
+7 -3
View File
@@ -225,7 +225,11 @@
v-model="driverForm.drivingVehicle"
maxlength="30"
clearable
placeholder="请输入或选择车辆"
readonly
placeholder="请选择车辆"
:disabled="readonly"
@clear="driverForm.drivingVehicle = ''"
@click="!readonly && openVehicleSelector()"
>
<template #append>
<el-tooltip content="选择车辆" placement="top">
@@ -582,8 +586,8 @@
<template #organizationName="{ row }">
{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}
</template>
<template #boundDriver>
<span>-</span>
<template #boundDriver="{ row }">
<span>{{ row.boundDriver || '-' }}</span>
</template>
<template #certificationStatus="{ row }">
<el-tag :type="getVehicleCertificationStatus(row).type" class="status-text">
+16 -12
View File
@@ -66,8 +66,8 @@
{{ row.status === 1 ? '启用' : '停用' }}
</el-tag>
</template>
<template #boundDriver>
<span>-</span>
<template #boundDriver="{ row }">
<span>{{ row.boundDriver || '-' }}</span>
</template>
<template #certificationStatus="{ row }">
<el-tag :type="getCertificationStatus(row).type" class="status-text">
@@ -541,19 +541,14 @@
</el-col>
<el-col :span="6">
<el-form-item label="使用部门" prop="useDepartment">
<el-select
v-model="vehicleForm.useDepartment"
<el-cascader
v-model="useDepartmentCascaderValue"
:options="organizationTreeOptions"
:props="organizationCascaderProps"
filterable
clearable
placeholder="请选择"
>
<el-option
v-for="item in organizationOptions"
:key="item.id"
:label="item.rawLabel"
:value="item.rawLabel"
/>
</el-select>
/>
</el-form-item>
</el-col>
</el-row>
@@ -908,6 +903,15 @@ export default {
this.vehicleForm.organizationName = this.resolveOrgName(path);
},
},
// 使用部门级联:与所属组织同一套部门树
useDepartmentCascaderValue: {
get() {
return this.resolveOrgPath(this.vehicleForm.useDepartment);
},
set(path) {
this.vehicleForm.useDepartment = this.resolveOrgName(path);
},
},
isAdmin() {
const authority = this.userInfo.authority || '';
return authority.includes('admin');
+151 -46
View File
@@ -57,6 +57,36 @@
{{ accessTypeMap[row.accessType] || '临时客商' }}
</el-tag>
</template>
<template #businessEndDate="{ row }">
<span :class="{ 'business-license-expiring': isBusinessLicenseExpiringSoon(row) }">
{{ formatBusinessLicenseValidPeriod(row) }}
</span>
</template>
<template #fundUseRate="{ row }">
{{ formatFundUseRate(row) }}
</template>
<template #fundUseRisk="{ row }">
<el-tag v-if="row.fundUseRisk === 'high'" type="danger">
{{ row.fundUseRiskName || '高风险' }}
</el-tag>
<el-tag v-else-if="row.fundUseRisk === 'medium'" type="warning">
{{ row.fundUseRiskName || '中风险' }}
</el-tag>
<span v-else class="fund-use-risk-none">-</span>
</template>
<template #fundUseRisk-header>
<span class="fund-use-risk-header">
资金使用额度风险
<el-tooltip placement="top" effect="dark">
<template #content>
<div>中风险80% 客户资金额度使用率 &lt; 90%</div>
<div>高风险客户资金额度使用率 90%</div>
<div>使用率 = (累计付款金额 - 累计收票金额) / 最大资金使用额度 × 100%</div>
</template>
<i class="el-icon-question fund-use-risk-help" />
</el-tooltip>
</span>
</template>
<template #approvalStatus="{ row }">
<el-tag
class="status-text"
@@ -176,11 +206,29 @@
<el-form-item label="客商编号" prop="customerCode">
<el-input
v-model="archiveForm.customerCode"
disabled
placeholder="保存后自动生成"
maxlength="20"
show-word-limit
placeholder="请输入,留空则自动生成"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="客商类型" prop="customerKind">
<el-select
v-model="archiveForm.customerKind"
placeholder="请选择"
filterable
clearable
>
<el-option
v-for="item in customerKindOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="客商性质" prop="customerNature">
<el-select
@@ -241,23 +289,6 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="客商类型" prop="customerKind">
<el-select
v-model="archiveForm.customerKind"
placeholder="请选择"
filterable
clearable
>
<el-option
v-for="item in customerKindOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="企业法人" prop="legalPerson">
<el-input v-model="archiveForm.legalPerson" maxlength="10" show-word-limit />
@@ -1676,6 +1707,7 @@ export default {
rejected: { label: '审核不通过', type: 'danger' },
},
formRules: {
customerCode: [{ max: 20, message: '客商编号最多20个字符', trigger: 'blur' }],
fullName: [{ required: true, message: '请输入客商名称', trigger: 'blur' }],
customerNature: [{ required: true, message: '请选择客商性质', trigger: 'change' }],
unifiedCreditCode: [
@@ -1856,6 +1888,24 @@ export default {
{ label: '正式', value: 'formal' },
],
},
{
label: '营业执照有效期',
prop: 'businessEndDate',
slot: true,
minWidth: 140,
},
{
label: '资金使用情况',
prop: 'fundUseRate',
slot: true,
minWidth: 130,
},
{
label: '资金使用额度风险',
prop: 'fundUseRisk',
slot: true,
minWidth: 150,
},
{
label: '审批状态',
prop: 'approvalStatus',
@@ -2098,6 +2148,7 @@ export default {
accessType: 'temporary',
approvalStatus: 'draft',
status: 1,
customerKind: 'external',
deptId: [],
deptIds: '',
deptName: '',
@@ -2268,6 +2319,40 @@ export default {
if (typeof normalized === 'object') return Object.keys(normalized).length === 0;
return false;
},
normalizeComparableChangeValue(value) {
const normalized = this.normalizeChangeFieldValue(value);
if (this.isEmptyChangeValue(normalized)) return null;
if (typeof normalized === 'number') return Number.isFinite(normalized) ? normalized : null;
if (typeof normalized === 'boolean') return normalized;
if (typeof normalized === 'string') {
const text = normalized.trim();
if (/^-?\d+(\.\d+)?$/.test(text)) {
const number = Number(text);
return Number.isFinite(number) ? number : text;
}
return text;
}
if (Array.isArray(normalized)) {
return normalized.map(item => this.normalizeComparableChangeValue(item));
}
if (normalized && typeof normalized === 'object') {
return Object.keys(normalized)
.sort()
.reduce((result, key) => {
result[key] = this.normalizeComparableChangeValue(normalized[key]);
return result;
}, {});
}
return normalized;
},
isSameChangeValue(beforeValue, afterValue) {
if (beforeValue === afterValue) return true;
if (this.isEmptyChangeValue(beforeValue) && this.isEmptyChangeValue(afterValue)) return true;
return (
JSON.stringify(this.normalizeComparableChangeValue(beforeValue)) ===
JSON.stringify(this.normalizeComparableChangeValue(afterValue))
);
},
formatScoreChangeValue(value) {
const scores = Array.isArray(value) ? value : [value];
const categoryNames = { basic: '基础得分项', plus: '加分项目', minus: '减分项目' };
@@ -2354,7 +2439,7 @@ export default {
fields.forEach(([field, label]) => {
const leftValue = left[field];
const rightValue = right[field];
if (JSON.stringify(leftValue) === JSON.stringify(rightValue)) return;
if (this.isSameChangeValue(leftValue, rightValue)) return;
const prefix = size > 1 ? `第${index + 1}条-${label}` : label;
before.push(`${prefix}${formatValue(field, leftValue)}`);
after.push(`${prefix}${formatValue(field, rightValue)}`);
@@ -2437,11 +2522,7 @@ export default {
const rows = fields
.filter(
field =>
field !== '变更内容' &&
!(
this.isEmptyChangeValue(beforeData[field]) &&
this.isEmptyChangeValue(afterData[field])
)
field !== '变更内容' && !this.isSameChangeValue(beforeData[field], afterData[field])
)
.map(field => ({
field: this.getChangeFieldLabel(field),
@@ -2451,7 +2532,8 @@ export default {
before: this.formatChangeFieldValue(field, beforeData[field]),
after: this.formatChangeFieldValue(field, afterData[field]),
}),
}));
}))
.filter(item => item.before !== item.after);
const content = String(row.changeContent || '').trim();
if (content) {
rows.unshift({ field: '变更内容', before: '空', after: content });
@@ -2467,27 +2549,11 @@ export default {
this.changeRecordDetailVisible = true;
},
formatChangeContent(row) {
const beforeData = this.parseChangeData(row.beforeData);
const afterData = this.parseChangeData(row.afterData);
const fields = [...new Set([...Object.keys(beforeData), ...Object.keys(afterData)])];
const details = fields
.filter(
field =>
field !== '变更内容' &&
!(
this.isEmptyChangeValue(beforeData[field]) &&
this.isEmptyChangeValue(afterData[field])
)
)
.map(field => {
const scoreDiff = this.isScoreChangeField(field)
? this.formatScoreChangeDiff(beforeData[field], afterData[field])
: null;
const before = scoreDiff?.before || this.formatChangeFieldValue(field, beforeData[field]);
const after = scoreDiff?.after || this.formatChangeFieldValue(field, afterData[field]);
return `${this.getChangeFieldLabel(field)}:变更前:${before} → 变更后:${after}`;
});
const rows = this.buildChangeRecordDetailRows(row);
const content = String(row.changeContent || '').trim();
const details = rows
.filter(item => item.field !== '变更内容')
.map(item => `${item.field}:变更前:${item.before} → 变更后:${item.after}`);
return [content, ...details].filter(Boolean).join('');
},
emptyContact() {
@@ -2833,6 +2899,25 @@ export default {
}
});
},
formatBusinessLicenseValidPeriod(row = {}) {
if (row.businessTermType === '长期') return '无固定期限';
return row.businessEndDate || '';
},
formatFundUseRate(row = {}) {
if (row.fundUseRate === undefined || row.fundUseRate === null || row.fundUseRate === '') {
return '-';
}
const rate = Number(row.fundUseRate);
return Number.isFinite(rate) ? `${rate.toFixed(2)}%` : '-';
},
isBusinessLicenseExpiringSoon(row = {}) {
if (row.businessTermType === '长期' || !row.businessEndDate) return false;
const endDate = this.$dayjs(row.businessEndDate);
if (!endDate.isValid()) return false;
const today = this.$dayjs().startOf('day');
const expireSoonDate = today.add(30, 'day');
return !endDate.isAfter(expireSoonDate, 'day');
},
handleBusinessEndDateChange(value) {
if (value) {
this.archiveForm.businessTermType = '固定期限';
@@ -4737,6 +4822,26 @@ export default {
--el-tag-text-color: #409eff;
}
.business-license-expiring {
color: var(--el-color-danger);
}
.fund-use-risk-none {
color: #c0c4cc;
}
.fund-use-risk-header {
display: inline-flex;
align-items: center;
}
.fund-use-risk-help {
margin-left: 4px;
color: #a8abb2;
cursor: help;
font-size: 14px;
}
.archive-form {
:deep(.el-form-item__label) {
flex: 0 0 calc(6em + 24px) !important;