✨ 评分减分累计与最高扣分、用户部门标签联动树,并修复对账导入与结算调整等
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+31
-10
@@ -105,12 +105,27 @@ const isJsonContentType = res => {
|
||||
return contentType.includes('application/json') || contentType.includes('text/json');
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否为可按文本读取的 Blob / Blob-like(含跨 realm)。
|
||||
* 禁止对普通业务 JSON 对象调用 .text()。
|
||||
*/
|
||||
const isBlobLikeData = value =>
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.size === 'number' &&
|
||||
typeof value.type === 'string' &&
|
||||
typeof value.text === 'function';
|
||||
|
||||
/**
|
||||
* 判断响应体是否已是解析好的对象(无需再解析)。
|
||||
* 注意 Blob 也是 object,必须排除,否则会把待解析的文件流误判为已完成。
|
||||
*/
|
||||
const isPlainObject = value =>
|
||||
value !== null && typeof value === 'object' && !(value instanceof Blob);
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
!(value instanceof Blob) &&
|
||||
!isBlobLikeData(value) &&
|
||||
!(value instanceof ArrayBuffer);
|
||||
//跨域请求,允许保存cookie
|
||||
axios.defaults.withCredentials = true;
|
||||
// NProgress Configuration
|
||||
@@ -190,15 +205,20 @@ axios.interceptors.response.use(
|
||||
// 用户就会以为系统崩了——而后端其实返回了明确的业务提示(如「未读取到数据行…」)。
|
||||
// 这里按 Content-Type 判断:JSON 才解析回对象;真·文件流(Excel 等)保持原样不动。
|
||||
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
|
||||
// 仅对真正的 Blob/Blob-like 调 .text(),避免业务 JSON 已解析后出现
|
||||
// 「e.data.text is not a function」。
|
||||
if (isJsonContentType(res) && !isPlainObject(res.data)) {
|
||||
try {
|
||||
let text = '';
|
||||
if (typeof res.data?.text === 'function') {
|
||||
text = await res.data.text();
|
||||
} else if (typeof res.data === 'string') {
|
||||
text = res.data;
|
||||
} else if (typeof res.data?.arrayBuffer === 'function') {
|
||||
text = new TextDecoder().decode(await res.data.arrayBuffer());
|
||||
const raw = res.data;
|
||||
if (isBlobLikeData(raw) || (typeof Blob !== 'undefined' && raw instanceof Blob)) {
|
||||
text = await raw.text();
|
||||
} else if (typeof raw === 'string') {
|
||||
text = raw;
|
||||
} else if (raw && typeof raw.arrayBuffer === 'function') {
|
||||
text = new TextDecoder().decode(await raw.arrayBuffer());
|
||||
} else if (raw instanceof ArrayBuffer) {
|
||||
text = new TextDecoder().decode(raw);
|
||||
}
|
||||
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
@@ -208,10 +228,11 @@ axios.interceptors.response.use(
|
||||
// 声明是 JSON 却解析失败:保持原样,交给下游按文件处理
|
||||
}
|
||||
}
|
||||
//获取状态信息
|
||||
const status = res.data.error_code || res.data.code || res.status;
|
||||
// 获取状态信息:文件流没有 code/msg,必须退回 HTTP status,禁止读 Blob 上的幽灵字段
|
||||
const payload = isPlainObject(res.data) ? res.data : {};
|
||||
const status = Number(payload.error_code || payload.code || res.status);
|
||||
const statusWhiteList = website.statusWhiteList || [];
|
||||
const message = res.data.msg || res.data.error_description || '系统错误';
|
||||
const message = payload.msg || payload.error_description || '系统错误';
|
||||
//如果在白名单里则自行catch逻辑处理
|
||||
if (statusWhiteList.includes(status)) return Promise.reject(res);
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ export const parseBlobJson = async data => {
|
||||
*
|
||||
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
|
||||
*/
|
||||
const resolveErrorMessage = async error => {
|
||||
export const resolveImportErrorMessage = async error => {
|
||||
const payload = error?.response?.data ?? error?.data;
|
||||
if (isBusinessJson(payload) && payload.msg) {
|
||||
return payload.msg;
|
||||
@@ -109,6 +109,26 @@ const resolveErrorMessage = async error => {
|
||||
return fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一消费导入接口响应:成功 JSON / 失败 Excel 流。
|
||||
* 兼容 axios 已把 JSON Blob 解析成对象的情况,禁止再对普通对象调 .text()。
|
||||
*/
|
||||
export const consumeImportResponse = async (res, options = {}) => {
|
||||
const { businessName = '导入', dayjsFormat } = options;
|
||||
if (isExcelResponse(res)) {
|
||||
return {
|
||||
kind: 'excel',
|
||||
blob: res?.data,
|
||||
fileName: `${businessName}导入失败明细${dayjsFormat || ''}.xlsx`,
|
||||
};
|
||||
}
|
||||
const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data);
|
||||
if (Number(result?.code) !== 200) {
|
||||
throw new Error(result?.msg || '导入失败');
|
||||
}
|
||||
return { kind: 'success', result };
|
||||
};
|
||||
|
||||
/**
|
||||
* 加工后端返回的导入失败明细 Excel。
|
||||
* 若传入 failDetailDecorator(函数),则动态加载 exceljs 读取 blob 流,
|
||||
@@ -235,7 +255,7 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
|
||||
// 只保留一次清晰的中文提示。
|
||||
if (!error?._notified) {
|
||||
vm.$message.error(await resolveErrorMessage(error));
|
||||
vm.$message.error(await resolveImportErrorMessage(error));
|
||||
}
|
||||
notifyUploadError(option, error);
|
||||
} finally {
|
||||
|
||||
@@ -13824,13 +13824,13 @@ export default {
|
||||
}
|
||||
const unavailableWaybills = this.selectionList.filter(
|
||||
row =>
|
||||
this.statusValue(row) !== 'pending' ||
|
||||
!['pending', 'processing', 'running'].includes(this.statusValue(row)) ||
|
||||
row.loadingId ||
|
||||
row.loadingManageId ||
|
||||
row.loadingNo
|
||||
);
|
||||
if (unavailableWaybills.length) {
|
||||
this.$message.warning('公路配载仅支持进行中且未配载的运单');
|
||||
this.$message.warning('公路配载仅支持待执行/进行中且未关联有效配载单的运单');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="途经地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly" placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly" placeholder="请输入详细地址" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人" class="route-contact-form-item"><el-input v-model="route.departureContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||
<el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" :disabled="isBasicRouteReadonly" placeholder="请输入" /><template v-if="!isBasicRouteReadonly"><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></template></div></el-form-item>
|
||||
@@ -111,7 +111,7 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="收货地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="isBasicRouteReadonly" placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly" placeholder="请输入详细地址" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人" class="route-contact-form-item"
|
||||
><el-input v-model="form.arrivalContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||
@@ -714,7 +714,7 @@ export default {
|
||||
'msg',
|
||||
'zip',
|
||||
],
|
||||
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
||||
transports: ['铁路运输', '水路运输', '航空运输', '公路运输'],
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
form: {
|
||||
@@ -798,6 +798,7 @@ export default {
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
defaultQuantityUnit() {
|
||||
if (this.quantityUnitOptions.includes('吨')) return '吨';
|
||||
return this.quantityUnitOptions[0] || '吨';
|
||||
},
|
||||
loadQuantityUnitOptions() {
|
||||
@@ -1164,22 +1165,30 @@ export default {
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return false;
|
||||
const siteCode = String(field.model[field.siteCode] || '').trim();
|
||||
// 地址展示模式由实际选择的数据决定:有站点编码表示非公路站点名称,
|
||||
// 没有站点编码则保留省市区级联,避免切换运输方式时改写已有地址。
|
||||
// 展示模式由已选数据决定:有站点编码则显示站点名,否则省市区;暂不按运输方式强制站点限制。
|
||||
return Boolean(siteCode && siteCode !== '/');
|
||||
},
|
||||
hasAddressValue(target) {
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return false;
|
||||
return Boolean(
|
||||
String(field.model[field.address] || '').trim() || String(field.model[field.name] || '').trim()
|
||||
);
|
||||
},
|
||||
tipEmptyAddressDetail(target) {
|
||||
// 替代原「请先选择运输方式」placeholder:空地址点击时 toast,已有值不再提示。
|
||||
if (!this.hasAddressValue(target)) {
|
||||
this.$message.warning('请输入详细地址');
|
||||
}
|
||||
},
|
||||
resolveAddressRegionCode(row = {}) {
|
||||
return row.regionCode || row.districtCode || row.areaCode || row.adcode || '';
|
||||
},
|
||||
openCommonAddress(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.tipEmptyAddressDetail(target);
|
||||
this.addressTarget = target;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.stationQuery = {};
|
||||
this.stationPage.current = 1;
|
||||
this.stationDialogVisible = true;
|
||||
return;
|
||||
}
|
||||
// 暂不按运输方式限制站点:统一走常用地址选择。
|
||||
this.addressQuery = {};
|
||||
this.addressPage.current = 1;
|
||||
this.addressDialogVisible = true;
|
||||
@@ -1279,18 +1288,11 @@ export default {
|
||||
},
|
||||
handleAddressDetailClick(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.openCommonAddress(target);
|
||||
return;
|
||||
}
|
||||
this.tipEmptyAddressDetail(target);
|
||||
this.openAddressMap(target);
|
||||
},
|
||||
openAddressMap(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.$message.info('非公路运输地址不能使用地图选择');
|
||||
return;
|
||||
}
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return;
|
||||
this.mapTarget = target;
|
||||
@@ -1690,7 +1692,11 @@ export default {
|
||||
'',
|
||||
cargoTypePath,
|
||||
quantity: row.quantity || '',
|
||||
quantityUnit: row.quantityUnit || this.defaultQuantityUnit(),
|
||||
// 常用货物单位若为空或「个」,新建总单统一默认「吨」
|
||||
quantityUnit:
|
||||
!row.quantityUnit || String(row.quantityUnit).trim() === '个'
|
||||
? this.defaultQuantityUnit()
|
||||
: row.quantityUnit,
|
||||
packageType: row.packageType || row.package || '',
|
||||
brand: row.brand || '',
|
||||
specification: row.specification || row.spec || '',
|
||||
@@ -1775,17 +1781,21 @@ export default {
|
||||
return formatUpdateUserName(row);
|
||||
},
|
||||
selectRoute(route) {
|
||||
// 先写入运输方式(会触发地点清空),再回填线路地址,避免被清空逻辑覆盖。
|
||||
this.form.finalTransportType = '公路运输';
|
||||
if (!this.form.routes.length) this.addRoute();
|
||||
if (this.form.routes[0]) this.form.routes[0].transportType = '公路运输';
|
||||
this.form.departureName = route.departureName || '';
|
||||
this.form.departureAddress = route.departureAddress || '';
|
||||
this.form.departureContact = route.departureContact || '';
|
||||
this.form.departurePhone = route.departurePhone || '';
|
||||
this.form.departureSiteCode = '';
|
||||
this.form.arrivalName = route.arrivalName || '';
|
||||
this.form.arrivalAddress = route.arrivalAddress || '';
|
||||
this.form.arrivalContact = route.arrivalContact || '';
|
||||
this.form.arrivalPhone = route.arrivalPhone || '';
|
||||
this.form.arrivalSiteCode = '';
|
||||
this.syncAddressRegionPaths();
|
||||
this.form.finalTransportType = '公路运输';
|
||||
if (!this.form.routes.length) this.addRoute();
|
||||
this.routeDialogVisible = false;
|
||||
},
|
||||
createRoute(index = this.form.routes.length) {
|
||||
|
||||
@@ -10414,13 +10414,13 @@ export default {
|
||||
}
|
||||
const unavailableWaybills = this.selectionList.filter(
|
||||
row =>
|
||||
!['processing', 'running'].includes(this.statusValue(row)) ||
|
||||
!['pending', 'processing', 'running'].includes(this.statusValue(row)) ||
|
||||
row.loadingId ||
|
||||
row.loadingManageId ||
|
||||
row.loadingNo
|
||||
);
|
||||
if (unavailableWaybills.length) {
|
||||
this.$message.warning('公路配载仅支持进行中且未配载的运单');
|
||||
this.$message.warning('公路配载仅支持待执行/进行中且未关联有效配载单的运单');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -635,10 +635,11 @@
|
||||
min-width="100" /><el-table-column
|
||||
prop="quantity"
|
||||
label="数量"
|
||||
min-width="90" /><el-table-column
|
||||
prop="quantityUnit"
|
||||
label="数量单位"
|
||||
min-width="100" /><el-table-column
|
||||
min-width="120"
|
||||
><template #default="{ row }">{{
|
||||
formatCargoQuantityDisplay(row)
|
||||
}}</template></el-table-column
|
||||
><el-table-column
|
||||
prop="materialCode"
|
||||
label="物料编码"
|
||||
min-width="120" /><el-table-column
|
||||
@@ -897,6 +898,16 @@
|
||||
>
|
||||
<div title="配载信息" class="loading-manage-dialog__load-info-card">
|
||||
<section-card>
|
||||
<div class="loading-manage-dialog__admission-tip">
|
||||
<div class="loading-manage-dialog__admission-title">可配载条件</div>
|
||||
<div class="loading-manage-dialog__admission-text">
|
||||
同时满足以下条件的运单可被配载:运输方式为「公路」;运单状态为「待执行」或「进行中」;未关联其他有效配载单。
|
||||
</div>
|
||||
<div class="loading-manage-dialog__admission-title">配载后状态说明</div>
|
||||
<div class="loading-manage-dialog__admission-text">
|
||||
子运单:按项目过程配置是否需司机接单,无需接单则为进行中,需接单则为待执行。配载单:按所选运单所属项目的过程配置,需接单则为待执行,无需接单则为进行中。
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-manage-dialog__candidate-search">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
@@ -1425,8 +1436,9 @@
|
||||
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
|
||||
<el-table-column prop="cargoType" label="货物类型" min-width="140" />
|
||||
<el-table-column prop="packageType" label="包装" min-width="100" />
|
||||
<el-table-column prop="quantity" label="数量" min-width="100" />
|
||||
<el-table-column prop="quantityUnit" label="数量单位" min-width="100" />
|
||||
<el-table-column prop="quantity" label="数量" min-width="120">
|
||||
<template #default="{ row }">{{ formatCargoQuantityDisplay(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||||
<el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
|
||||
<el-table-column prop="brand" label="品牌" min-width="120" />
|
||||
@@ -3900,12 +3912,10 @@ export default {
|
||||
try {
|
||||
const query = {
|
||||
...this.buildQueryParams(this.candidateQuery),
|
||||
// 待执行(需接单未接)与进行中(无需接单或已接单)均可配载
|
||||
businessStatus: 'pending,running',
|
||||
onlyUnassignedLoading: 1,
|
||||
// 与后端 LoadableAdmissionSupport 一致:公路 + 待执行/进行中 + 未关联有效配载单
|
||||
loadableOnly: 1,
|
||||
// 仅查当前账户所属组织及子组织下的运单
|
||||
allDept: 0,
|
||||
...(this.dialogMode === 'add' ? { transportType: 'road' } : {}),
|
||||
};
|
||||
const transportType = this.getCandidateTransportTypeQuery(query.transportType);
|
||||
const res = await getWaybillList(
|
||||
@@ -4195,14 +4205,28 @@ export default {
|
||||
return [this.normalizeCargoRow(row, row)];
|
||||
});
|
||||
},
|
||||
formatCargoQuantityDisplay(row = {}) {
|
||||
const quantity = blankCargoSentinel(row.quantity ?? row.cargoQuantity ?? '');
|
||||
const unit = String(
|
||||
blankCargoSentinel(
|
||||
row.quantityUnit ||
|
||||
row.goodsQuantityUnit ||
|
||||
row.cargoUnit ||
|
||||
row.unit ||
|
||||
''
|
||||
) || ''
|
||||
).trim();
|
||||
if (quantity === '' || quantity === undefined || quantity === null) {
|
||||
return unit || '';
|
||||
}
|
||||
return unit ? `${quantity} ${unit}` : String(quantity);
|
||||
},
|
||||
normalizeCargoRow(waybill, item = {}) {
|
||||
return sanitizeCargoRow({
|
||||
projectName: item.projectName || waybill.projectName || '',
|
||||
cargoName: item.cargoName || item.goodsName || waybill.cargoName || '',
|
||||
cargoType: item.cargoType || item.goodsType || waybill.cargoType || '',
|
||||
packageType: item.packageType || item.package || '',
|
||||
weight: item.weight || item.cargoWeight || '',
|
||||
volume: item.volume || item.cargoVolume || '',
|
||||
quantity: item.quantity || item.cargoQuantity || waybill.quantity || '',
|
||||
quantityUnit:
|
||||
item.quantityUnit ||
|
||||
@@ -4399,8 +4423,8 @@ export default {
|
||||
try {
|
||||
const payload = {
|
||||
...this.normalizePayload(),
|
||||
// 进行中编辑保持 running;其余确认生成正式配载单为待执行
|
||||
businessStatus: this.isRunningEdit ? 'running' : 'pending',
|
||||
// 进行中编辑保持 running;新建/草稿确认由后端按项目是否需接单写入 pending/running
|
||||
...(this.isRunningEdit ? { businessStatus: 'running' } : {}),
|
||||
};
|
||||
if (this.dialogMode === 'reassign') {
|
||||
await loadingApi.reassign(payload);
|
||||
@@ -4969,6 +4993,30 @@ export default {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__admission-tip {
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 14px;
|
||||
background: #f5f9ff;
|
||||
border: 1px solid #d9ecff;
|
||||
border-radius: 4px;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__admission-title {
|
||||
margin-bottom: 4px;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__admission-title + .loading-manage-dialog__admission-text + .loading-manage-dialog__admission-title {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__admission-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__candidate-search {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -203,7 +203,11 @@
|
||||
@click="openBatchDialog(row)"
|
||||
>更换运单批次</el-link
|
||||
><el-link
|
||||
v-if="(row.processStatus !== '处理完成' || row.auditStatus === '待审核') && row.auditStatus !== '审核驳回'"
|
||||
v-if="
|
||||
row.processStatus !== '处理失败' &&
|
||||
(row.processStatus !== '处理完成' || row.auditStatus === '待审核') &&
|
||||
row.auditStatus !== '审核驳回'
|
||||
"
|
||||
type="primary"
|
||||
@click="openBatchDialog(row)"
|
||||
>更换运单批次</el-link
|
||||
@@ -320,7 +324,7 @@
|
||||
<el-form-item label="执行凭证" required
|
||||
><el-upload
|
||||
action="#"
|
||||
accept=".zip,.7z"
|
||||
accept=".zip"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:on-change="selectVoucherFile"
|
||||
@@ -332,7 +336,7 @@
|
||||
>上传</el-button
|
||||
><template #tip
|
||||
><span class="voucher-manage-page__upload-tip"
|
||||
>请上传车辆的执行凭证压缩包,支持zip、7z格式。 上传附件大于50M时为超大附件,确认提交后可在“批量补录->上传任务”中查看上传进度。</span
|
||||
>请上传车辆的执行凭证压缩包,仅支持 zip 格式;无法解析的文件将标记为「处理失败」,可重新上传。上传附件大于50M时为超大附件,确认提交后可在“批量补录->上传任务”中查看上传进度。</span
|
||||
></template
|
||||
></el-upload
|
||||
><el-progress
|
||||
@@ -549,7 +553,7 @@
|
||||
ref="resumeFileInput"
|
||||
class="voucher-manage-page__hidden-file"
|
||||
type="file"
|
||||
accept=".zip,.7z"
|
||||
accept=".zip"
|
||||
@change="resumeFileSelected"
|
||||
/>
|
||||
</template>
|
||||
@@ -998,6 +1002,12 @@ const uploadVoucher = async file => {
|
||||
};
|
||||
const selectVoucherFile = file => {
|
||||
if (!file?.raw) return;
|
||||
const fileName = String(file.raw.name || '').toLowerCase();
|
||||
if (!fileName.endsWith('.zip')) {
|
||||
ElMessage.warning('仅支持 zip 压缩包,无法解析的文件将标记为处理失败');
|
||||
clearFile();
|
||||
return;
|
||||
}
|
||||
selectedVoucherFile.value = file.raw;
|
||||
editing.fileName = file.raw.name;
|
||||
editing.fileUrl = '';
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
<el-table-column prop="paymentRatio" label="付款比例(%)" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="multipleSettlementRows && !readonly"
|
||||
v-if="!readonly"
|
||||
:model-value="row.paymentRatio"
|
||||
:min="0"
|
||||
:max="rowPaymentRatioMax(row)"
|
||||
@@ -262,7 +262,7 @@
|
||||
<el-table-column prop="appliedAmount" label="付款金额" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="multipleSettlementRows && !readonly"
|
||||
v-if="!readonly"
|
||||
:model-value="row.appliedAmount"
|
||||
:min="0"
|
||||
:max="Number(row.payableAmount || 0)"
|
||||
@@ -1014,62 +1014,37 @@ export default {
|
||||
referenceSettlementCount() {
|
||||
return this.referenceSettlementNos.length;
|
||||
},
|
||||
multipleSettlementRows() {
|
||||
return this.referenceRows.length > 1;
|
||||
},
|
||||
// 结算单号展示:单张显示单号;多张仅提示数量,明细在下方「结算信息」表,悬停可看全部单号
|
||||
referenceDisplayText() {
|
||||
if (this.referenceSettlementCount > 1) return `已选 ${this.referenceSettlementCount} 张结算单`;
|
||||
return this.referenceLabel;
|
||||
},
|
||||
settlementRows() {
|
||||
if (this.referenceRows.length > 1) {
|
||||
// 多结算单:行内比例/金额独立维护,不回落到表头比例(表头与结算信息不关联)
|
||||
return this.referenceRows.map(item => {
|
||||
const paymentRatio =
|
||||
item.paymentRatio === undefined || item.paymentRatio === null || item.paymentRatio === ''
|
||||
? 0
|
||||
: Number(item.paymentRatio);
|
||||
const appliedAmount = Number(item.appliedAmount || 0);
|
||||
return {
|
||||
...item,
|
||||
paymentRatio,
|
||||
appliedAmount,
|
||||
...this.buildSettlementRatioHint({ ...item, paymentRatio }),
|
||||
};
|
||||
});
|
||||
}
|
||||
if (this.referenceRows.length) {
|
||||
return this.referenceRows.map(item => {
|
||||
const appliedAmount =
|
||||
item.fixedAppliedAmount === true
|
||||
? Number(item.appliedAmount || 0)
|
||||
: Number(
|
||||
(
|
||||
(Number(item.settlementAmount || 0) * Number(this.form.paymentRatio || 0)) /
|
||||
100
|
||||
).toFixed(2)
|
||||
);
|
||||
return {
|
||||
...item,
|
||||
paymentRatio: this.form.paymentRatio,
|
||||
appliedAmount,
|
||||
...this.buildSettlementRatioHint(item),
|
||||
};
|
||||
});
|
||||
}
|
||||
if (!this.referenceLabel) return [];
|
||||
return [
|
||||
{
|
||||
settlementNo: this.referenceLabel,
|
||||
settlementAmount: this.form.settlementAmount,
|
||||
payableAmount: this.form.payableAmount,
|
||||
paymentRatio: this.form.paymentRatio,
|
||||
appliedAmount: this.form.appliedAmount,
|
||||
paymentSequence: 1,
|
||||
...this.buildSettlementRatioHint({ paymentSequence: 1 }),
|
||||
},
|
||||
];
|
||||
const source = this.referenceRows.length
|
||||
? this.referenceRows
|
||||
: this.referenceLabel
|
||||
? [
|
||||
{
|
||||
settlementNo: this.referenceLabel,
|
||||
settlementAmount: this.form.settlementAmount,
|
||||
payableAmount: this.form.payableAmount,
|
||||
paymentRatio: null,
|
||||
appliedAmount: null,
|
||||
paymentSequence: 1,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
// 结算行比例/金额只取行内数据,不回落到付款基本信息
|
||||
return source.map(item => {
|
||||
const paymentRatio = this.nullableNumber(item.paymentRatio);
|
||||
const appliedAmount = this.nullableNumber(item.appliedAmount);
|
||||
return {
|
||||
...item,
|
||||
paymentRatio,
|
||||
appliedAmount,
|
||||
...this.buildSettlementRatioHint({ ...item, paymentRatio }),
|
||||
};
|
||||
});
|
||||
},
|
||||
highRiskPayee() {
|
||||
const level = String(this.payeeCustomerLevel || '')
|
||||
@@ -1127,11 +1102,12 @@ export default {
|
||||
if (
|
||||
this.amountSyncing ||
|
||||
this.form.paymentType === 'project_advance' ||
|
||||
!this.paymentAmountBase
|
||||
!this.paymentAmountBase ||
|
||||
this.nullableNumber(this.form.paymentRatio) === null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// 结算金额变化时按上方比例重算申请付款金额,仍不联动结算信息明细
|
||||
// 结算金额变化时只按上方比例重算申请付款金额,不改结算信息明细
|
||||
this.amountSyncing = true;
|
||||
this.form.appliedAmount = Number(
|
||||
((this.paymentAmountBase * Number(this.form.paymentRatio || 0)) / 100).toFixed(2)
|
||||
@@ -1532,8 +1508,6 @@ export default {
|
||||
paymentSequence: amount.paymentSequence,
|
||||
}),
|
||||
settlementNo: item.formalSettlementNo || item.settlementNo || '',
|
||||
appliedAmount: Number(item.appliedAmount || 0),
|
||||
fixedAppliedAmount: true,
|
||||
};
|
||||
});
|
||||
this.seedMultipleRowPayments(referenceRows);
|
||||
@@ -1545,27 +1519,6 @@ export default {
|
||||
(total, item) => total + Number(item.payableAmount || 0),
|
||||
0
|
||||
);
|
||||
const multiple = referenceRows.length > 1;
|
||||
// 多结算单:表头比例/申请金额独立;单结算单:沿用行金额或按比例计算
|
||||
const paymentRatio = Number(this.form.paymentRatio || 0);
|
||||
const rowsAppliedTotal = referenceRows.reduce(
|
||||
(total, item) => total + Number(item.appliedAmount || 0),
|
||||
0
|
||||
);
|
||||
const headerAppliedAmount = multiple
|
||||
? Number(((settlementAmount * paymentRatio) / 100).toFixed(2))
|
||||
: Number(
|
||||
(
|
||||
rowsAppliedTotal > 0
|
||||
? rowsAppliedTotal
|
||||
: (payableAmount * paymentRatio) / 100
|
||||
).toFixed(2)
|
||||
);
|
||||
const headerPaymentRatio = multiple
|
||||
? paymentRatio
|
||||
: settlementAmount
|
||||
? Number(((headerAppliedAmount / settlementAmount) * 100).toFixed(2))
|
||||
: paymentRatio;
|
||||
const settlementIds = rows.map(item => item.settlementId || item.id).filter(Boolean);
|
||||
this.amountSyncing = true;
|
||||
this.referenceRows = referenceRows;
|
||||
@@ -1592,8 +1545,7 @@ export default {
|
||||
settlementAmount: Number(settlementAmount.toFixed(2)),
|
||||
payableAmount: Number(payableAmount.toFixed(2)),
|
||||
billType: '',
|
||||
paymentRatio: headerPaymentRatio,
|
||||
appliedAmount: headerAppliedAmount,
|
||||
appliedAmount: this.headerAppliedAmountByRatio(settlementAmount),
|
||||
invoices: rows.flatMap(item =>
|
||||
this.createInvoiceRows(item.invoices || [], item.formalSettlementNo || item.settlementNo)
|
||||
),
|
||||
@@ -1649,7 +1601,7 @@ export default {
|
||||
referenceType: isPre ? 'pre' : 'formal',
|
||||
settlementAmount: Number(Number(item.settlementAmount || 0).toFixed(2)),
|
||||
payableAmount: Number(Number(item.payableAmount ?? item.settlementAmount ?? 0).toFixed(2)),
|
||||
appliedAmount: Number(Number(item.appliedAmount || 0).toFixed(2)),
|
||||
appliedAmount: this.nullableNumber(item.appliedAmount),
|
||||
paymentRatio: Number(item.settlementAmount || 0)
|
||||
? Number(
|
||||
(
|
||||
@@ -1658,11 +1610,10 @@ export default {
|
||||
).toFixed(2)
|
||||
)
|
||||
: null,
|
||||
fixedAppliedAmount: this.readonly,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (rows.length > 1) {
|
||||
if (rows.length) {
|
||||
this.form.preSettlementIds = rows.filter(item => item.referenceType === 'pre').map(item => item.id);
|
||||
this.form.settlementIds = rows.filter(item => item.referenceType === 'formal').map(item => item.id);
|
||||
if (this.form.preSettlementIds.length) this.form.preSettlementId = this.form.preSettlementIds[0];
|
||||
@@ -1696,18 +1647,27 @@ export default {
|
||||
settlementAmount: Number(Number(settlementAmount || 0).toFixed(2)),
|
||||
payableAmount: Number(Math.max(0, payableAmount).toFixed(2)),
|
||||
paymentSequence: Number.isFinite(paymentSequence) && paymentSequence > 0 ? paymentSequence : 1,
|
||||
paymentRatio: null,
|
||||
appliedAmount: null,
|
||||
};
|
||||
},
|
||||
nullableNumber(value) {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
const amount = Number(value);
|
||||
return Number.isFinite(amount) ? amount : null;
|
||||
},
|
||||
/** 付款基本信息内部:已填写比例时,按结算金额重算申请付款金额;不读取结算行付款金额。 */
|
||||
headerAppliedAmountByRatio(settlementAmount) {
|
||||
if (this.nullableNumber(this.form.paymentRatio) === null) return this.form.appliedAmount;
|
||||
return Number(((Number(settlementAmount || 0) * Number(this.form.paymentRatio)) / 100).toFixed(2));
|
||||
},
|
||||
/**
|
||||
* 按合同付款比例配置,判断当前付款次序是否超上限。
|
||||
* @param {Object} row 结算信息行(含 paymentSequence)
|
||||
*/
|
||||
buildSettlementRatioHint(row = {}) {
|
||||
const sequence = Number(row.paymentSequence || 1);
|
||||
const rawRatio =
|
||||
row.paymentRatio !== undefined && row.paymentRatio !== null && row.paymentRatio !== ''
|
||||
? row.paymentRatio
|
||||
: this.form.paymentRatio;
|
||||
const rawRatio = row.paymentRatio;
|
||||
const paymentRatio = Number(rawRatio);
|
||||
const ratioConfig = this.contractPaymentRatioRows[sequence - 1];
|
||||
const ratioLimitValue = ratioConfig?.ratioLimit;
|
||||
@@ -1850,21 +1810,6 @@ export default {
|
||||
(total, item) => total + Number(item.payableAmount || 0),
|
||||
0
|
||||
);
|
||||
const multiple = this.referenceRows.length > 1;
|
||||
const paymentRatio = Number(this.form.paymentRatio || 0);
|
||||
const rowsAppliedTotal = this.referenceRows.reduce(
|
||||
(total, item) => total + Number(item.appliedAmount || 0),
|
||||
0
|
||||
);
|
||||
// 多结算单:表头申请金额只由上方比例计算;单结算单仍按可付款金额×比例
|
||||
const headerAppliedAmount = multiple
|
||||
? Number(((settlementAmount * paymentRatio) / 100).toFixed(2))
|
||||
: Number(((payableAmount * paymentRatio) / 100).toFixed(2));
|
||||
const headerPaymentRatio = multiple
|
||||
? paymentRatio
|
||||
: settlementAmount && rowsAppliedTotal > 0
|
||||
? Number(((rowsAppliedTotal / settlementAmount) * 100).toFixed(2))
|
||||
: paymentRatio;
|
||||
// 尾款付款场景下选择预付结算单时保留尾款付款类型,不强制改为进度预付
|
||||
this.amountSyncing = true;
|
||||
Object.assign(this.form, {
|
||||
@@ -1890,8 +1835,7 @@ export default {
|
||||
settlementAmount: Number(settlementAmount.toFixed(2)),
|
||||
payableAmount: Number(payableAmount.toFixed(2)),
|
||||
billType: '',
|
||||
paymentRatio: headerPaymentRatio,
|
||||
appliedAmount: headerAppliedAmount,
|
||||
appliedAmount: this.headerAppliedAmountByRatio(settlementAmount),
|
||||
});
|
||||
this.rememberSettlementParties(first.payerName, first.payeeName);
|
||||
this.paymentRecords = [];
|
||||
@@ -3080,22 +3024,54 @@ export default {
|
||||
return Number(Math.min(100, (payableAmount / settlementAmount) * 100).toFixed(2));
|
||||
},
|
||||
/**
|
||||
* 多结算单行金额规范化:仅在行内反算比例,不使用表头比例灌入(表头与结算信息不关联)。
|
||||
* 结算行金额规范化:只在行内反算比例,不使用付款基本信息的比例和申请金额。
|
||||
*/
|
||||
seedMultipleRowPayments(rows = []) {
|
||||
if (rows.length <= 1) return;
|
||||
rows.forEach(item => {
|
||||
const settlementAmount = Number(item.settlementAmount || 0);
|
||||
const payableAmount = Number(item.payableAmount || 0);
|
||||
let applied = Number(item.appliedAmount || 0);
|
||||
if (!Number.isFinite(applied) || applied < 0) applied = 0;
|
||||
if (applied > payableAmount) applied = Number(payableAmount.toFixed(2));
|
||||
item.appliedAmount = Number(applied.toFixed(2));
|
||||
item.paymentRatio = settlementAmount
|
||||
? Number(((item.appliedAmount / settlementAmount) * 100).toFixed(2))
|
||||
: 0;
|
||||
if (this.nullableNumber(item.appliedAmount) === null && this.nullableNumber(item.paymentRatio) === null) {
|
||||
item.appliedAmount = null;
|
||||
item.paymentRatio = null;
|
||||
return;
|
||||
}
|
||||
this.syncRowPayment(item, 'amount');
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 结算行付款比例与付款金额联动。比例 = 付款金额 / 结算总金额。
|
||||
* @param {'ratio'|'amount'} source 以哪一侧为准
|
||||
*/
|
||||
syncRowPayment(row, source) {
|
||||
const settlementAmount = Number(row.settlementAmount || 0);
|
||||
const payableAmount = Number(row.payableAmount || 0);
|
||||
if (source === 'ratio') {
|
||||
const ratio = this.nullableNumber(row.paymentRatio);
|
||||
if (ratio === null) {
|
||||
row.paymentRatio = null;
|
||||
row.appliedAmount = null;
|
||||
return;
|
||||
}
|
||||
let applied = Number(((settlementAmount * ratio) / 100).toFixed(2));
|
||||
if (applied > payableAmount) applied = Number(payableAmount.toFixed(2));
|
||||
row.appliedAmount = applied;
|
||||
row.paymentRatio = settlementAmount
|
||||
? Number(((applied / settlementAmount) * 100).toFixed(2))
|
||||
: Number(ratio.toFixed(2));
|
||||
return;
|
||||
}
|
||||
const appliedValue = this.nullableNumber(row.appliedAmount);
|
||||
if (appliedValue === null) {
|
||||
row.appliedAmount = null;
|
||||
row.paymentRatio = null;
|
||||
return;
|
||||
}
|
||||
let applied = appliedValue;
|
||||
if (applied < 0) applied = 0;
|
||||
if (applied > payableAmount) applied = payableAmount;
|
||||
row.appliedAmount = Number(applied.toFixed(2));
|
||||
row.paymentRatio = settlementAmount
|
||||
? Number(((row.appliedAmount / settlementAmount) * 100).toFixed(2))
|
||||
: 0;
|
||||
},
|
||||
findReferenceRow(row) {
|
||||
return this.referenceRows.find(
|
||||
item =>
|
||||
@@ -3104,55 +3080,19 @@ export default {
|
||||
item.settlementNo === row.settlementNo
|
||||
);
|
||||
},
|
||||
/** 结算信息付款金额合计 */
|
||||
settlementRowsAppliedTotal(rows = this.referenceRows) {
|
||||
return (rows || []).reduce((total, item) => total + Number(item.appliedAmount || 0), 0);
|
||||
},
|
||||
/** 结算信息合计不得超过上方申请付款金额 */
|
||||
ensureSettlementAppliedWithinHeader(showWarning = true) {
|
||||
if (!this.multipleSettlementRows) return true;
|
||||
const headerApplied = Number(this.form.appliedAmount || 0);
|
||||
const rowsTotal = Number(this.settlementRowsAppliedTotal().toFixed(2));
|
||||
if (rowsTotal - headerApplied > 1e-8) {
|
||||
if (showWarning) {
|
||||
this.$message.warning(
|
||||
`结算信息付款金额合计(${rowsTotal.toFixed(2)})不能大于申请付款金额(${headerApplied.toFixed(2)})`
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handleRowPaymentRatioChange(row, value) {
|
||||
const target = this.findReferenceRow(row);
|
||||
if (!target) return;
|
||||
const settlementAmount = Number(target.settlementAmount || 0);
|
||||
const payableAmount = Number(target.payableAmount || 0);
|
||||
const ratio = value === null || value === undefined || value === '' ? 0 : Number(value);
|
||||
let applied = Number(((settlementAmount * ratio) / 100).toFixed(2));
|
||||
if (applied > payableAmount) applied = Number(payableAmount.toFixed(2));
|
||||
target.appliedAmount = applied;
|
||||
target.paymentRatio = settlementAmount
|
||||
? Number(((applied / settlementAmount) * 100).toFixed(2))
|
||||
: Number(ratio.toFixed(2));
|
||||
// 仅刷新结算金额/可付款金额合计,不回写表头申请金额与比例
|
||||
target.paymentRatio = value;
|
||||
this.syncRowPayment(target, 'ratio');
|
||||
this.syncHeaderSettlementTotalsFromRows();
|
||||
this.ensureSettlementAppliedWithinHeader(true);
|
||||
},
|
||||
handleRowAppliedAmountChange(row, value) {
|
||||
const target = this.findReferenceRow(row);
|
||||
if (!target) return;
|
||||
const settlementAmount = Number(target.settlementAmount || 0);
|
||||
const payableAmount = Number(target.payableAmount || 0);
|
||||
let applied = value === null || value === undefined || value === '' ? 0 : Number(value);
|
||||
if (!Number.isFinite(applied) || applied < 0) applied = 0;
|
||||
if (applied > payableAmount) applied = payableAmount;
|
||||
target.appliedAmount = Number(applied.toFixed(2));
|
||||
target.paymentRatio = settlementAmount
|
||||
? Number(((target.appliedAmount / settlementAmount) * 100).toFixed(2))
|
||||
: 0;
|
||||
target.appliedAmount = value;
|
||||
this.syncRowPayment(target, 'amount');
|
||||
this.syncHeaderSettlementTotalsFromRows();
|
||||
this.ensureSettlementAppliedWithinHeader(true);
|
||||
},
|
||||
/** 多结算单时仅同步结算金额/可付款金额合计,申请付款金额与比例由上方独立维护 */
|
||||
syncHeaderSettlementTotalsFromRows() {
|
||||
@@ -3261,8 +3201,8 @@ export default {
|
||||
settlementLines: (this.referenceRows || []).map(item => ({
|
||||
formalSettlementId: item.referenceType === 'formal' ? item.id : null,
|
||||
preSettlementId: item.referenceType === 'pre' ? item.id : null,
|
||||
paymentRatio: item.paymentRatio,
|
||||
appliedAmount: item.appliedAmount,
|
||||
paymentRatio: this.nullableNumber(item.paymentRatio),
|
||||
appliedAmount: this.nullableNumber(item.appliedAmount),
|
||||
})),
|
||||
paymentMethod,
|
||||
billLedgerId,
|
||||
@@ -3323,21 +3263,22 @@ export default {
|
||||
ElMessage.warning('申请付款金额必填且必须大于0');
|
||||
return false;
|
||||
}
|
||||
if (this.multipleSettlementRows) {
|
||||
const invalidRow = this.referenceRows.find(
|
||||
item => Number(item.appliedAmount || 0) > Number(item.payableAmount || 0)
|
||||
);
|
||||
if (this.isSettlementRelatedType && this.referenceRows.length) {
|
||||
const invalidRow = this.referenceRows.find(item => {
|
||||
const rowApplied = Number(item.appliedAmount || 0);
|
||||
return !Number.isFinite(rowApplied) || rowApplied <= 0;
|
||||
});
|
||||
if (invalidRow) {
|
||||
this.clearMessages();
|
||||
ElMessage.warning(`${invalidRow.settlementNo || '结算单'}付款金额不能超过可付款金额`);
|
||||
ElMessage.warning(`${invalidRow.settlementNo || '结算单'}请填写付款金额,且必须大于0`);
|
||||
return false;
|
||||
}
|
||||
const rowsTotal = Number(this.settlementRowsAppliedTotal().toFixed(2));
|
||||
if (rowsTotal - appliedAmount > 1e-8) {
|
||||
const exceededRow = this.referenceRows.find(
|
||||
item => Number(item.appliedAmount || 0) > Number(item.payableAmount || 0)
|
||||
);
|
||||
if (exceededRow) {
|
||||
this.clearMessages();
|
||||
ElMessage.warning(
|
||||
`结算信息付款金额合计(${rowsTotal.toFixed(2)})不能大于申请付款金额(${appliedAmount.toFixed(2)})`
|
||||
);
|
||||
ElMessage.warning(`${exceededRow.settlementNo || '结算单'}付款金额不能超过可付款金额`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +380,13 @@ export default {
|
||||
},
|
||||
async loadFormalSettlements(keyword = '') {
|
||||
const { data } = await api.getFormalSettlements(keyword);
|
||||
this.candidates = data?.data || data || [];
|
||||
const payload = data?.data ?? data;
|
||||
// 兼容误返回分页对象的情况,只取正式结算单列表
|
||||
this.candidates = Array.isArray(payload)
|
||||
? payload
|
||||
: Array.isArray(payload?.records)
|
||||
? payload.records
|
||||
: [];
|
||||
},
|
||||
async loadFeeOptions() {
|
||||
const { data } = await api.getFeeOptions();
|
||||
|
||||
@@ -926,14 +926,34 @@ const stripInternalRowFields = row => {
|
||||
};
|
||||
};
|
||||
|
||||
/** 合并费用列名与各行 feeItems 键,避免保存/回显时丢动态费用列 */
|
||||
const collectFeeItemNames = (preferred = [], detailRows = []) => {
|
||||
const names = [];
|
||||
const seen = new Set();
|
||||
const pushName = value => {
|
||||
const name = String(value || '').trim();
|
||||
if (!name || seen.has(name)) return;
|
||||
seen.add(name);
|
||||
names.push(name);
|
||||
};
|
||||
(preferred || []).forEach(pushName);
|
||||
(detailRows || []).forEach(row => {
|
||||
const feeItems = row?.feeItems;
|
||||
if (!feeItems || typeof feeItems !== 'object' || Array.isArray(feeItems)) return;
|
||||
Object.keys(feeItems).forEach(pushName);
|
||||
});
|
||||
return names;
|
||||
};
|
||||
|
||||
const buildPayload = () => {
|
||||
const { file, ...batch } = form;
|
||||
const payloadRows = rows.value.map(stripInternalRowFields);
|
||||
return {
|
||||
...batch,
|
||||
settlementBillType: form.settlementType === 'payable' ? form.settlementBillType || 'formal' : 'formal',
|
||||
waybillMode: form.waybillMode === 'history' ? 'history' : 'new',
|
||||
feeItemNames: [...feeItemNames.value],
|
||||
rows: rows.value.map(stripInternalRowFields),
|
||||
feeItemNames: collectFeeItemNames(feeItemNames.value, payloadRows),
|
||||
rows: payloadRows,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1436,9 +1456,13 @@ const loadDetail = async id => {
|
||||
if (detail.projectId) {
|
||||
await loadContractsByProject(detail.projectId, detail.projectName || '');
|
||||
}
|
||||
feeItemNames.value = Array.isArray(detail.feeItemNames) ? detail.feeItemNames : [];
|
||||
parsedHeaders.value = [];
|
||||
const detailRows = Array.isArray(detail.rows) ? detail.rows : [];
|
||||
// 先按库中列名与明细 feeItems 合并,再规范化行数据,保证草稿再打开仍展示全部费用列
|
||||
feeItemNames.value = collectFeeItemNames(
|
||||
Array.isArray(detail.feeItemNames) ? detail.feeItemNames : [],
|
||||
detailRows
|
||||
);
|
||||
rows.value = detailRows.map((item, index) => normalizeParsedRow(item, index));
|
||||
refreshDuplicateFlags();
|
||||
// 查看模式始终只读;编辑时仅草稿可改,已完成/已作废强制只读
|
||||
|
||||
@@ -125,15 +125,17 @@
|
||||
<span class="reconciliation-editor__filter-label">货物名称</span>
|
||||
<el-input v-model="internalQuery.cargoName" placeholder="请输入" clearable />
|
||||
</div>
|
||||
<el-button @click="resetInternalQuery">重置</el-button>
|
||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!filteredInternalRows.length"
|
||||
@click="exportInternalDetails"
|
||||
>导出</el-button
|
||||
>
|
||||
<div class="reconciliation-editor__filter-actions">
|
||||
<el-button @click="resetInternalQuery">重置</el-button>
|
||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!filteredInternalRows.length"
|
||||
@click="exportInternalDetails"
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
:data="filteredInternalRows"
|
||||
@@ -189,6 +191,9 @@
|
||||
"
|
||||
>{{ formatDate(row[column.prop]) }}</span
|
||||
>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{
|
||||
formatOptionalMoney(row.unitPrice)
|
||||
}}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -318,6 +323,9 @@
|
||||
"
|
||||
>{{ formatDate(row[column.prop]) }}</span
|
||||
>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{
|
||||
formatOptionalMoney(row.unitPrice)
|
||||
}}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -476,6 +484,7 @@
|
||||
<span v-else-if="column.feeItemName">{{ formatMoney(getFeeItemAmount(row, column.feeItemName)) }}</span>
|
||||
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
|
||||
<span v-else-if="column.prop === 'actualDepartureTime' || column.prop === 'actualCompletionTime'">{{ formatDate(row[column.prop]) }}</span>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{ formatOptionalMoney(row.unitPrice) }}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -630,7 +639,10 @@ import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { h } from 'vue';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { isExcelResponse, parseBlobJson } from '@/utils/import-excel';
|
||||
import {
|
||||
consumeImportResponse,
|
||||
resolveImportErrorMessage,
|
||||
} from '@/utils/import-excel';
|
||||
import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm';
|
||||
import {
|
||||
internalColumns,
|
||||
@@ -1042,12 +1054,24 @@ export default {
|
||||
.map(normalized)
|
||||
.join('|');
|
||||
},
|
||||
blankOptionalNumber(value) {
|
||||
if (value === null || value === undefined || value === '' || Number(value) === -1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
normalizeInternalRow(row) {
|
||||
const feeItems =
|
||||
row.feeItems && typeof row.feeItems === 'object' && Object.keys(row.feeItems).length
|
||||
? row.feeItems
|
||||
: this.parseFeeItems(row.feeItemsJson);
|
||||
return { ...row, feeItems };
|
||||
return {
|
||||
...row,
|
||||
feeItems,
|
||||
unitPrice: this.blankOptionalNumber(row.unitPrice),
|
||||
mileage: this.blankOptionalNumber(row.mileage),
|
||||
freightAmount: this.blankOptionalNumber(row.freightAmount),
|
||||
};
|
||||
},
|
||||
normalizeExternalRow(row, index) {
|
||||
const feeItems =
|
||||
@@ -1057,6 +1081,9 @@ export default {
|
||||
return {
|
||||
...row,
|
||||
feeItems,
|
||||
unitPrice: this.blankOptionalNumber(row.unitPrice),
|
||||
mileage: this.blankOptionalNumber(row.mileage),
|
||||
freightAmount: this.blankOptionalNumber(row.freightAmount),
|
||||
_externalKey: row.id || row.externalLineNo || `external-${index}`,
|
||||
};
|
||||
},
|
||||
@@ -1284,6 +1311,9 @@ export default {
|
||||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||||
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
|
||||
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
|
||||
snapshot.unitPrice = this.blankOptionalNumber(row.unitPrice);
|
||||
snapshot.mileage = this.blankOptionalNumber(row.mileage);
|
||||
snapshot.freightAmount = this.blankOptionalNumber(row.freightAmount);
|
||||
return snapshot;
|
||||
});
|
||||
},
|
||||
@@ -1297,6 +1327,9 @@ export default {
|
||||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||||
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
|
||||
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
|
||||
snapshot.unitPrice = this.blankOptionalNumber(row.unitPrice);
|
||||
snapshot.mileage = this.blankOptionalNumber(row.mileage);
|
||||
snapshot.freightAmount = this.blankOptionalNumber(row.freightAmount);
|
||||
snapshot.suspectedDuplicate = duplicateIds.has(String(row.id));
|
||||
return snapshot;
|
||||
});
|
||||
@@ -1326,7 +1359,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async handleUpdate() {
|
||||
await this.$confirm('将以外部账单数据更新运单及内部结算明细,是否继续?', '更新账单', {
|
||||
await this.$confirm('将以外部账单数据更新内部结算明细,是否继续?', '更新账单', {
|
||||
type: 'warning',
|
||||
});
|
||||
this.actionLoading = true;
|
||||
@@ -1508,22 +1541,23 @@ export default {
|
||||
? await api.importCargo(this.currentId, file)
|
||||
: await api.importVehicle(this.currentId, file);
|
||||
// 成功时 axios 拦截器可能已把 JSON Blob 解析成对象,不能再调 .text()
|
||||
if (isExcelResponse(response)) {
|
||||
downloadXls(
|
||||
response.data,
|
||||
`运输对账导入失败明细${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`
|
||||
);
|
||||
const consumed = await consumeImportResponse(response, {
|
||||
businessName: '运输对账',
|
||||
dayjsFormat: this.$dayjs().format('YYYY-MM-DD HH-mm-ss'),
|
||||
});
|
||||
if (consumed.kind === 'excel') {
|
||||
downloadXls(consumed.blob, consumed.fileName);
|
||||
this.$message.warning('部分数据导入失败,已下载失败明细');
|
||||
} else {
|
||||
const result = await parseBlobJson(response.data);
|
||||
if (Number(result.code) !== 200) throw new Error(result.msg || '导入失败');
|
||||
this.$message.success('外部账单导入成功');
|
||||
}
|
||||
this.matchingStarted = false;
|
||||
await this.loadDetail();
|
||||
this.matchingStarted = false;
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '外部账单导入失败');
|
||||
if (!error?._notified) {
|
||||
this.$message.error(await resolveImportErrorMessage(error));
|
||||
}
|
||||
}
|
||||
},
|
||||
async downloadTemplate(mode) {
|
||||
@@ -1831,7 +1865,10 @@ export default {
|
||||
: Number(value || 0).toFixed(2);
|
||||
},
|
||||
formatMileage(value) {
|
||||
return Number(value) === -1 ? '' : this.displayValue(value);
|
||||
return this.blankOptionalNumber(value) === null ? '' : this.displayValue(value);
|
||||
},
|
||||
formatOptionalMoney(value, currency = 'RMB') {
|
||||
return this.blankOptionalNumber(value) === null ? '-' : this.formatMoney(value, currency);
|
||||
},
|
||||
formatDate(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
@@ -1952,15 +1989,26 @@ export default {
|
||||
}
|
||||
.reconciliation-editor__filter {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto auto;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.reconciliation-editor__filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.reconciliation-editor__filter-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.reconciliation-editor__filter-actions .el-button {
|
||||
min-width: 64px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.reconciliation-editor__filter-label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 8px;
|
||||
|
||||
@@ -6,15 +6,33 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, getCurrentInstance, nextTick, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import SettlementImportPage from './components/settlement-import-page.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
if (route.query.mode === 'view') return '查看导入结算单';
|
||||
return route.query.id ? '编辑导入结算单' : '新增导入结算单';
|
||||
});
|
||||
|
||||
const syncTagTitle = () => {
|
||||
nextTick(() => {
|
||||
store.commit('SET_TAG', {
|
||||
fullPath: route.fullPath,
|
||||
name: pageTitle.value,
|
||||
});
|
||||
instance?.proxy?.$router?.$avueRouter?.setTitle?.(pageTitle.value);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
syncTagTitle();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -239,19 +239,22 @@ export default {
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
openCreate() {
|
||||
this.$router.push({ path: '/settlement/settlement-import/form' });
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { name: '新增导入结算单' },
|
||||
});
|
||||
},
|
||||
openDetail(row) {
|
||||
if (!row?.id) return;
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { id: row.id, mode: 'view' },
|
||||
query: { id: row.id, mode: 'view', name: '查看导入结算单' },
|
||||
});
|
||||
},
|
||||
editDraft(row) {
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { id: row.id },
|
||||
query: { id: row.id, name: '编辑导入结算单' },
|
||||
});
|
||||
},
|
||||
async handleDelete(row) {
|
||||
|
||||
+105
-2
@@ -115,9 +115,10 @@
|
||||
<template #deptName="{ row }">
|
||||
<template v-if="formatDeptNames(row.deptName).length">
|
||||
<el-tag
|
||||
v-for="name in formatDeptNames(row.deptName)"
|
||||
:key="name"
|
||||
v-for="(name, index) in formatDeptNames(row.deptName)"
|
||||
:key="`${row.id}-${index}-${name}`"
|
||||
class="user-dept-tag"
|
||||
@click.stop="handleDeptTagClick(row, index)"
|
||||
>{{ name }}</el-tag>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
@@ -666,6 +667,104 @@ export default {
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
formatDeptIds(deptId) {
|
||||
if (!deptId && deptId !== 0) {
|
||||
return [];
|
||||
}
|
||||
return String(deptId)
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
handleDeptTagClick(row, index) {
|
||||
const deptIds = this.formatDeptIds(row.deptId);
|
||||
let deptId = deptIds[index];
|
||||
if (!deptId) {
|
||||
const names = this.formatDeptNames(row.deptName);
|
||||
deptId = this.findDeptIdByName(names[index]);
|
||||
}
|
||||
if (!deptId) {
|
||||
this.$message.warning('未找到对应组织');
|
||||
return;
|
||||
}
|
||||
this.selectDeptOnTree(deptId);
|
||||
},
|
||||
findDeptIdByName(name, tree = this.permissionDeptTree) {
|
||||
if (!name) return null;
|
||||
for (const node of tree || []) {
|
||||
if (node.title === name || node.deptName === name || node.label === name) {
|
||||
return node.id;
|
||||
}
|
||||
const matched = this.findDeptIdByName(name, node.children);
|
||||
if (matched != null) return matched;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* 展开左侧懒加载组织树到目标节点并设为选中,同时按该组织筛选用户列表
|
||||
*/
|
||||
async selectDeptOnTree(deptId) {
|
||||
if (deptId == null || deptId === '') return;
|
||||
if (!this.permissionDeptTree?.length) {
|
||||
const tenantId = this.userInfo.tenantId || this.website.tenantId;
|
||||
try {
|
||||
const res = await getDeptTree(tenantId);
|
||||
this.permissionDeptTree = res.data.data || [];
|
||||
} catch (error) {
|
||||
this.$message.error('组织树加载失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
const path = this.findDeptPath(deptId);
|
||||
if (!path?.length) {
|
||||
this.$message.warning('未在组织树中找到该部门');
|
||||
return;
|
||||
}
|
||||
const tree = this.$refs.deptTree;
|
||||
if (!tree) return;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
await this.expandLazyTreeNode(path[i]);
|
||||
}
|
||||
await this.$nextTick();
|
||||
const targetId = path[path.length - 1];
|
||||
tree.setCurrentKey?.(targetId);
|
||||
this.$nextTick(() => {
|
||||
const currentEl = tree.$el?.querySelector?.('.el-tree-node.is-current');
|
||||
currentEl?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' });
|
||||
});
|
||||
if (String(this.treeDeptId) === String(targetId)) {
|
||||
return;
|
||||
}
|
||||
this.treeDeptId = targetId;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
expandLazyTreeNode(key) {
|
||||
return new Promise(resolve => {
|
||||
const tree = this.$refs.deptTree;
|
||||
if (!tree?.getNode) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const tryExpand = (retries = 40) => {
|
||||
const node = tree.getNode(key);
|
||||
if (!node) {
|
||||
if (retries <= 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
setTimeout(() => tryExpand(retries - 1), 50);
|
||||
return;
|
||||
}
|
||||
if (node.expanded || node.isLeaf) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
node.expand(() => resolve());
|
||||
};
|
||||
tryExpand();
|
||||
});
|
||||
},
|
||||
handleDropdownCommand(command, row, index) {
|
||||
if (command === 'setLeader') {
|
||||
this.handleSetLeader(row);
|
||||
@@ -1537,6 +1636,10 @@ export default {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.user-dept-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-dept-tag + .user-dept-tag {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
@@ -101,6 +101,17 @@
|
||||
style="width: 500px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="减分最高扣分">
|
||||
<el-input
|
||||
:model-value="configForm.maxMinusScore"
|
||||
placeholder="所有减分项合计不得超过该分值"
|
||||
clearable
|
||||
style="width: 500px"
|
||||
@input="handleMaxMinusScoreInput"
|
||||
>
|
||||
<template #append>分</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="configForm.remark"
|
||||
@@ -628,6 +639,7 @@ export default {
|
||||
name: '',
|
||||
remark: '',
|
||||
standardDescription: '',
|
||||
maxMinusScore: '',
|
||||
status: 3,
|
||||
categories: this.defaultCategories(),
|
||||
standards: [],
|
||||
@@ -684,6 +696,7 @@ export default {
|
||||
});
|
||||
return {
|
||||
...detail,
|
||||
maxMinusScore: this.normalizePlainDecimal(detail.maxMinusScore),
|
||||
categories: categories.map(category => ({
|
||||
...(map[category.categoryCode] || category),
|
||||
categoryName: category.categoryName,
|
||||
@@ -691,6 +704,14 @@ export default {
|
||||
standards: (detail.standards || []).map(standard => this.normalizeStandard(standard)),
|
||||
};
|
||||
},
|
||||
handleMaxMinusScoreInput(value) {
|
||||
const raw = String(value ?? '')
|
||||
.replace(/[^\d.]/g, '')
|
||||
.replace(/^\./, '')
|
||||
.replace(/(\..*)\./g, '$1')
|
||||
.replace(/^(\d+)(\.\d{0,2})?.*$/, '$1$2');
|
||||
this.configForm.maxMinusScore = raw;
|
||||
},
|
||||
openConfig(row, readonly = false) {
|
||||
this.readonly = readonly;
|
||||
if (row && row.id) {
|
||||
@@ -816,6 +837,9 @@ export default {
|
||||
},
|
||||
prepareSubmitConfig(config) {
|
||||
const payload = JSON.parse(JSON.stringify(config || {}));
|
||||
const maxMinus = this.normalizePlainDecimal(payload.maxMinusScore);
|
||||
payload.maxMinusScore =
|
||||
maxMinus === '' || maxMinus == null ? null : this.toScoreValue(maxMinus);
|
||||
(payload.categories || []).forEach(category => {
|
||||
(category.items || []).forEach(item => {
|
||||
item.score = this.toScoreValue(item.score);
|
||||
@@ -1072,6 +1096,14 @@ export default {
|
||||
this.$message.warning('请输入评定表名称');
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.configForm.maxMinusScore !== '' &&
|
||||
this.configForm.maxMinusScore != null &&
|
||||
!this.isNonNegativeDecimal(this.configForm.maxMinusScore)
|
||||
) {
|
||||
this.$message.warning('减分最高扣分须为不小于0的数字,最多两位小数');
|
||||
return false;
|
||||
}
|
||||
const basic = this.configForm.categories.find(item => item.categoryCode === 'basic');
|
||||
if (!basic || basic.items.length === 0) {
|
||||
this.$message.warning('至少配置1个基础得分项目');
|
||||
|
||||
@@ -592,18 +592,27 @@
|
||||
<span>{{ row.reviewStatus }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<el-table-column label="操作" min-width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)">
|
||||
详情
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="showScoreDelete"
|
||||
type="danger"
|
||||
@click="deleteScore(row.__rawIndex)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
<div class="score-list-table__actions">
|
||||
<el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)">
|
||||
详情
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="canReviewScore(row)"
|
||||
type="primary"
|
||||
@click="startReviewScore(row.__raw, row.__rawIndex)"
|
||||
>
|
||||
复评
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="showScoreDelete"
|
||||
type="danger"
|
||||
@click="deleteScore(row.__rawIndex)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -2231,7 +2240,8 @@ export default {
|
||||
showScoreDelete() {
|
||||
if (this.readonly) return false;
|
||||
const status = this.archiveForm.approvalStatus || 'draft';
|
||||
return status === 'draft' || status === 'rejected';
|
||||
// 草稿/驳回可删;审批通过后也允许删除并重建评分表(旧评分逻辑删除保留,新评分审核通过前仍用旧额度)
|
||||
return status === 'draft' || status === 'rejected' || status === 'approved';
|
||||
},
|
||||
isNewScore() {
|
||||
return this.scoreRecordIndex < 0 && !this.reviewMode;
|
||||
@@ -2281,10 +2291,17 @@ export default {
|
||||
{ categoryCode: 'plus', categoryName: '加分项目' },
|
||||
{ categoryCode: 'minus', categoryName: '减分项目' },
|
||||
];
|
||||
const maxMinus = Number(this.currentScore?.maxMinusScore);
|
||||
const hasMaxMinus = Number.isFinite(maxMinus) && maxMinus >= 0;
|
||||
const rows = categoryMap.map((category, index) => {
|
||||
const details = this.getScoreCategoryDetails(category.categoryCode);
|
||||
const selfScore = details.reduce((sum, item) => sum + Number(item.selfScore || 0), 0);
|
||||
const reviewScore = details.reduce((sum, item) => sum + Number(item.reviewScore || 0), 0);
|
||||
let selfScore = details.reduce((sum, item) => sum + Number(item.selfScore || 0), 0);
|
||||
let reviewScore = details.reduce((sum, item) => sum + Number(item.reviewScore || 0), 0);
|
||||
// 减分项目合计受量化表「最高扣分」限制
|
||||
if (category.categoryCode === 'minus' && hasMaxMinus) {
|
||||
selfScore = Math.min(selfScore, maxMinus);
|
||||
reviewScore = Math.min(reviewScore, maxMinus);
|
||||
}
|
||||
return {
|
||||
...category,
|
||||
index: index + 1,
|
||||
@@ -4199,6 +4216,7 @@ export default {
|
||||
scoreDate: this.$dayjs().format('YYYY-MM-DD'),
|
||||
applyCreditLimit: this.archiveForm.applyCreditLimit || '',
|
||||
maxCreditLimit: '',
|
||||
maxMinusScore: '',
|
||||
creditLevel: '',
|
||||
standards: [],
|
||||
tempApplyCreditLimit: '',
|
||||
@@ -4218,6 +4236,10 @@ export default {
|
||||
: score.applyCreditLimit
|
||||
),
|
||||
maxCreditLimit: this.normalizeOptionalAmount(score.maxCreditLimit),
|
||||
maxMinusScore:
|
||||
score.maxMinusScore === undefined || score.maxMinusScore === null
|
||||
? ''
|
||||
: score.maxMinusScore,
|
||||
tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit),
|
||||
attachments: score.attachments || this.parseAttachments(score.proofAttachments),
|
||||
details: (score.details || []).map(detail => ({
|
||||
@@ -4256,7 +4278,14 @@ export default {
|
||||
String(item.score.creditLevel || '').trim()
|
||||
)[0]?.score;
|
||||
},
|
||||
/** 曾审核通过后:删除/重建评分在再次审核通过前,主表等级与额度保持旧值暂时生效 */
|
||||
shouldFreezeEffectiveCredit(archive = this.archiveForm) {
|
||||
return Boolean(archive?.approvedTime);
|
||||
},
|
||||
applyArchiveCreditFromScores(archive) {
|
||||
if (this.shouldFreezeEffectiveCredit(archive)) {
|
||||
return archive;
|
||||
}
|
||||
const latestScore = this.getLatestCreditScore(archive.scores || []);
|
||||
archive.customerLevel = latestScore?.creditLevel || archive.customerLevel || '';
|
||||
archive.maxCreditLimit = latestScore?.maxCreditLimit || archive.maxCreditLimit || '';
|
||||
@@ -4588,9 +4617,31 @@ export default {
|
||||
}
|
||||
this.$nextTick(() => this.openScore(scores[0], 0));
|
||||
},
|
||||
/** 提交后详情页评分表:审批中且未完成复评时可复评 */
|
||||
canReviewScore(row = {}) {
|
||||
return (
|
||||
this.hasPermission('customer_type_re_cert') &&
|
||||
this.archiveForm.approvalStatus === 'reviewing' &&
|
||||
row.reviewStatus !== '已完成'
|
||||
);
|
||||
},
|
||||
startReviewScore(row, index = 0) {
|
||||
if (!row) {
|
||||
this.$message.warning('暂无评分记录,无法复评');
|
||||
return;
|
||||
}
|
||||
// 详情只读进入时也要能改复评得分并确认
|
||||
this.reviewMode = true;
|
||||
this.openScore(row, index);
|
||||
},
|
||||
addScore() {
|
||||
if ((this.archiveForm.scores || []).length) {
|
||||
this.$message.warning('请勿重复添加');
|
||||
const existingScores = this.archiveForm.scores || [];
|
||||
if (existingScores.length) {
|
||||
this.$message.warning(
|
||||
this.shouldFreezeEffectiveCredit()
|
||||
? '请先删除原评分记录后再添加新评分表'
|
||||
: '请勿重复添加'
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.scoreRecordIndex = -1;
|
||||
@@ -4611,9 +4662,9 @@ export default {
|
||||
const loadOptions = this.scoreQuantificationOptions.length
|
||||
? Promise.resolve()
|
||||
: this.initScoreQuantificationOptions();
|
||||
loadOptions.then(() =>
|
||||
this.ensureScoreQuantificationOption(this.currentScore.quantificationId)
|
||||
);
|
||||
loadOptions
|
||||
.then(() => this.ensureScoreQuantificationOption(this.currentScore.quantificationId))
|
||||
.then(() => this.applyMaxMinusScoreFromQuantification(this.currentScore.quantificationId));
|
||||
},
|
||||
resetScoreDialog() {
|
||||
this.scoreRecordIndex = -1;
|
||||
@@ -4648,6 +4699,11 @@ export default {
|
||||
applyCreditLimit: this.currentScore.applyCreditLimit,
|
||||
maxCreditLimit: '',
|
||||
creditLevel: '',
|
||||
maxMinusScore:
|
||||
quantificationData.maxMinusScore ??
|
||||
scoreTemplate.maxMinusScore ??
|
||||
this.currentScore.maxMinusScore ??
|
||||
'',
|
||||
tempApplyCreditLimit: this.currentScore.tempApplyCreditLimit,
|
||||
tempCreditStartDate: this.currentScore.tempCreditStartDate,
|
||||
tempCreditEndDate: this.currentScore.tempCreditEndDate,
|
||||
@@ -4664,6 +4720,17 @@ export default {
|
||||
this.calculateScore(this.currentScore);
|
||||
});
|
||||
},
|
||||
applyMaxMinusScoreFromQuantification(quantificationId) {
|
||||
if (!quantificationId) return Promise.resolve();
|
||||
return getCreditScoreQuantificationDetail(quantificationId).then(res => {
|
||||
const data = res?.data?.data || {};
|
||||
this.currentScore.maxMinusScore =
|
||||
data.maxMinusScore === undefined || data.maxMinusScore === null
|
||||
? ''
|
||||
: data.maxMinusScore;
|
||||
this.calculateScore(this.currentScore);
|
||||
});
|
||||
},
|
||||
normalizeScoreTemplateDetails(template = {}) {
|
||||
const categoryDetails = (template.categories || []).flatMap(category =>
|
||||
(category.items || []).map(item => ({
|
||||
@@ -4808,13 +4875,24 @@ export default {
|
||||
if (!valid) return null;
|
||||
|
||||
const distance = increase ? input - base : base - input;
|
||||
const changeValue = Number(rule.changeValue);
|
||||
const stepSize = Number.isFinite(changeValue) && changeValue > 0 ? changeValue : 1;
|
||||
const steps = Math.floor(distance / stepSize + 1e-9);
|
||||
const stepScore = Math.abs(Number(rule.score || 0));
|
||||
if (!Number.isFinite(stepScore)) return null;
|
||||
if (!Number.isFinite(stepScore) || steps < 0) return null;
|
||||
|
||||
const projectScore = this.getScoreItemFullScore(detail);
|
||||
// 减分项:按阶梯累计扣分,存正数,总分计算时再减;不做项目满分截断
|
||||
if (this.isMinusScoreDetail(detail)) {
|
||||
return Number((steps * stepScore).toFixed(2));
|
||||
}
|
||||
|
||||
// 基础/加分项:从项目分值起步,按阶梯加减,限制在 [0, 项目满分]
|
||||
const baseScore = Number(detail.score || 0);
|
||||
const signedStep = rule.scoreType === 'subtract' ? -stepScore : stepScore;
|
||||
const calculatedScore = Number(detail.score || 0) + distance * signedStep;
|
||||
return Number(Math.min(Math.max(calculatedScore, 0), projectScore).toFixed(2));
|
||||
const calculatedScore = baseScore + steps * signedStep;
|
||||
const projectScore = this.getScoreItemFullScore(detail);
|
||||
const upper = Math.max(projectScore, baseScore, 0);
|
||||
return Number(Math.min(Math.max(calculatedScore, 0), upper).toFixed(2));
|
||||
},
|
||||
handleScoreValueInput(detail, value) {
|
||||
const raw = String(value ?? '')
|
||||
@@ -4995,14 +5073,17 @@ export default {
|
||||
const plusDetails = this.getScoreCategoryDetailsByList(details, 'plus');
|
||||
const minusDetails = this.getScoreCategoryDetailsByList(details, 'minus');
|
||||
const sumScore = (list, prop) => list.reduce((sum, item) => sum + Number(item[prop] || 0), 0);
|
||||
const rawMinusSelf = sumScore(minusDetails, 'selfScore');
|
||||
const rawMinusReview = sumScore(minusDetails, 'reviewScore');
|
||||
// 量化表「减分最高扣分」:所有减分项合计不得超过该上限
|
||||
const maxMinus = Number(score.maxMinusScore);
|
||||
const hasMaxMinus = Number.isFinite(maxMinus) && maxMinus >= 0;
|
||||
const minusSelf = hasMaxMinus ? Math.min(rawMinusSelf, maxMinus) : rawMinusSelf;
|
||||
const minusReview = hasMaxMinus ? Math.min(rawMinusReview, maxMinus) : rawMinusReview;
|
||||
const selfScore =
|
||||
sumScore(basicDetails, 'selfScore') +
|
||||
sumScore(plusDetails, 'selfScore') -
|
||||
sumScore(minusDetails, 'selfScore');
|
||||
sumScore(basicDetails, 'selfScore') + sumScore(plusDetails, 'selfScore') - minusSelf;
|
||||
const reviewScore =
|
||||
sumScore(basicDetails, 'reviewScore') +
|
||||
sumScore(plusDetails, 'reviewScore') -
|
||||
sumScore(minusDetails, 'reviewScore');
|
||||
sumScore(basicDetails, 'reviewScore') + sumScore(plusDetails, 'reviewScore') - minusReview;
|
||||
const hasReviewScore = details.some(
|
||||
item => !this.isBlankScore(item.reviewScore)
|
||||
);
|
||||
@@ -5076,7 +5157,11 @@ export default {
|
||||
return pending;
|
||||
},
|
||||
deleteScore(index) {
|
||||
this.$confirm('确定删除该评分记录?', '提示', {
|
||||
const freezeCredit = this.shouldFreezeEffectiveCredit();
|
||||
const confirmText = freezeCredit
|
||||
? '确定删除该评分记录?删除后可重新添加评分表;原评分数据会保留,新评分再次审核通过前,客商等级与额度仍按原评分暂时生效。'
|
||||
: '确定删除该评分记录?';
|
||||
this.$confirm(confirmText, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
@@ -5089,16 +5174,23 @@ export default {
|
||||
} else if (this.scoreRecordIndex > index) {
|
||||
this.scoreRecordIndex -= 1;
|
||||
}
|
||||
const latestScore = this.getLatestCreditScore(this.archiveForm.scores);
|
||||
if (latestScore) {
|
||||
this.syncArchiveCreditFromScores();
|
||||
} else {
|
||||
this.archiveForm.customerLevel = '';
|
||||
this.archiveForm.maxCreditLimit = '';
|
||||
this.archiveForm.applyCreditLimit = '';
|
||||
if (!freezeCredit) {
|
||||
const latestScore = this.getLatestCreditScore(this.archiveForm.scores);
|
||||
if (latestScore) {
|
||||
this.syncArchiveCreditFromScores();
|
||||
} else {
|
||||
this.archiveForm.customerLevel = '';
|
||||
this.archiveForm.maxCreditLimit = '';
|
||||
this.archiveForm.applyCreditLimit = '';
|
||||
}
|
||||
}
|
||||
this.handleScoreCurrentChange(this.scorePage.currentPage);
|
||||
this.saveScoreArchive('评分记录已删除', '评分记录已删除,请保存客商档案后生效');
|
||||
this.saveScoreArchive(
|
||||
freezeCredit
|
||||
? '评分记录已删除,原等级与额度暂时继续生效'
|
||||
: '评分记录已删除',
|
||||
'评分记录已删除,请保存客商档案后生效'
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
@@ -5795,6 +5887,14 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.score-list-table__actions {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.contact-form,
|
||||
.receipt-form,
|
||||
.invoice-form {
|
||||
|
||||
+15
-15
@@ -49,28 +49,28 @@ export default ({ mode, command }) => {
|
||||
__VUE_I18N_LEGACY_API__: true,
|
||||
__INTLIFY_PROD_DEVTOOLS__: false,
|
||||
},
|
||||
// server: {
|
||||
// port: 2888,
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// // 本地网关默认 80 端口(blade-gateway bootstrap.yml);开发时需先启动网关及 blade-auth/blade-system 等服务
|
||||
// // 注意:本机 8080 一般是 Nacos,不是业务网关
|
||||
// target: 'http://localhost',
|
||||
// // target: 'http://172.16.203.228:8000', // 仅联调远程网关时启用(且不要 rewrite)
|
||||
// changeOrigin: true,
|
||||
// rewrite: path => path.replace(/^\/api/, ''),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
server: {
|
||||
port: 2889,
|
||||
port: 2888,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://172.16.203.228:8000',
|
||||
// 本地网关默认 80 端口(blade-gateway bootstrap.yml);开发时需先启动网关及 blade-auth/blade-system 等服务
|
||||
// 注意:本机 8080 一般是 Nacos,不是业务网关
|
||||
target: 'http://localhost',
|
||||
// target: 'http://172.16.203.228:8000', // 仅联调远程网关时启用(且不要 rewrite)
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
// server: {
|
||||
// port: 2889,
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: 'http://172.16.203.228:8000',
|
||||
// changeOrigin: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': resolve(__dirname, './'),
|
||||
|
||||
Reference in New Issue
Block a user