1、新增结算模块
2、调整业务模块 3、调整客商模块
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import request from '@/axios';
|
||||
|
||||
export const getList = (current, size, params) => {
|
||||
return request({
|
||||
url: '/blade-transport/insurance-ocr-template/list',
|
||||
method: 'get',
|
||||
params: {
|
||||
...params,
|
||||
current,
|
||||
size,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getDetail = id => {
|
||||
return request({
|
||||
url: '/blade-transport/insurance-ocr-template/detail',
|
||||
method: 'get',
|
||||
params: { id },
|
||||
});
|
||||
};
|
||||
|
||||
export const submit = row => {
|
||||
return request({
|
||||
url: '/blade-transport/insurance-ocr-template/submit',
|
||||
method: 'post',
|
||||
data: row,
|
||||
});
|
||||
};
|
||||
|
||||
export const remove = ids => {
|
||||
return request({
|
||||
url: '/blade-transport/insurance-ocr-template/remove',
|
||||
method: 'post',
|
||||
params: { ids },
|
||||
});
|
||||
};
|
||||
@@ -70,6 +70,13 @@ export const startChange = (id, reason) =>
|
||||
},
|
||||
});
|
||||
|
||||
export const submitChange = data =>
|
||||
request({
|
||||
url: `${baseUrl}/submit-change`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
|
||||
export const terminate = (id, reason) =>
|
||||
request({
|
||||
url: `${baseUrl}/terminate`,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import request from '@/axios';
|
||||
|
||||
const baseUrl = '/blade-transport/formal-settlement';
|
||||
|
||||
export const getList = (current, size, params) =>
|
||||
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
||||
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
||||
export const getCandidates = (current, size, params) =>
|
||||
request({
|
||||
url: `${baseUrl}/candidate-pre-settlements`,
|
||||
method: 'get',
|
||||
params: { current, size, ...params },
|
||||
});
|
||||
export const getContractOptions = keyword =>
|
||||
request({ url: `${baseUrl}/contract-options`, method: 'get', params: { keyword } });
|
||||
export const getCandidateDetails = (current, size, params) =>
|
||||
request({
|
||||
url: `${baseUrl}/candidate-details`,
|
||||
method: 'get',
|
||||
params: { current, size, ...params },
|
||||
});
|
||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
export const syncKingdee = id =>
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
|
||||
export const getDetailFees = detailId =>
|
||||
request({ url: `${baseUrl}/detail-fees`, method: 'get', params: { detailId } });
|
||||
export const adjustDetail = data =>
|
||||
request({ url: `${baseUrl}/adjust-detail`, method: 'post', data });
|
||||
export const applyPayment = data =>
|
||||
request({ url: `${baseUrl}/apply-payment`, method: 'post', data });
|
||||
@@ -0,0 +1,59 @@
|
||||
import request from '@/axios';
|
||||
|
||||
const baseUrl = '/blade-transport/pre-settlement';
|
||||
|
||||
export const getList = (current, size, params) =>
|
||||
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
||||
|
||||
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
||||
|
||||
export const getContractOptions = keyword =>
|
||||
request({ url: `${baseUrl}/contract-options`, method: 'get', params: { keyword } });
|
||||
|
||||
export const getFeeOptions = () => request({ url: `${baseUrl}/fee-options`, method: 'get' });
|
||||
|
||||
export const getCandidateDetails = (current, size, params) =>
|
||||
request({
|
||||
url: `${baseUrl}/candidate-details`,
|
||||
method: 'get',
|
||||
params: { current, size, ...params },
|
||||
});
|
||||
|
||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
|
||||
export const removeDetail = (id, detailId) =>
|
||||
request({ url: `${baseUrl}/remove-detail`, method: 'post', params: { id, detailId } });
|
||||
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
|
||||
export const applyAdvance = data =>
|
||||
request({ url: `${baseUrl}/apply-advance`, method: 'post', data });
|
||||
|
||||
export const updateAdvancePaid = params =>
|
||||
request({ url: `${baseUrl}/update-advance-paid`, method: 'post', params });
|
||||
|
||||
export const voidAdvance = params =>
|
||||
request({ url: `${baseUrl}/void-advance`, method: 'post', params });
|
||||
|
||||
export const formalSettlement = id =>
|
||||
request({ url: `${baseUrl}/formal-settlement`, method: 'post', params: { id } });
|
||||
|
||||
export const getDetailFees = detailId =>
|
||||
request({ url: `${baseUrl}/detail-fees`, method: 'get', params: { detailId } });
|
||||
|
||||
export const adjustDetail = data =>
|
||||
request({ url: `${baseUrl}/adjust-detail`, method: 'post', data });
|
||||
|
||||
export const getPrintTemplates = id =>
|
||||
request({ url: `${baseUrl}/print-templates`, method: 'get', params: { id } });
|
||||
|
||||
export const exportList = params =>
|
||||
request({ url: `${baseUrl}/export`, method: 'get', params, responseType: 'blob' });
|
||||
@@ -18,6 +18,12 @@ export const getChangeRecords = (current, size, params) =>
|
||||
export const updateFee = data =>
|
||||
request({ url: `${baseUrl}/update-fee`, method: 'post', data });
|
||||
|
||||
export const getUpdateFeeContracts = params =>
|
||||
request({ url: `${baseUrl}/update-fee-contracts`, method: 'get', params });
|
||||
|
||||
export const adjustFee = data =>
|
||||
request({ url: `${baseUrl}/adjust-fee`, method: 'post', data });
|
||||
|
||||
export const transferSettlement = data =>
|
||||
request({ url: `${baseUrl}/transfer-settlement`, method: 'post', data });
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import request from '@/axios';
|
||||
|
||||
const baseUrl = '/blade-transport/settlement-adjustment';
|
||||
export const getList = (current, size, params) =>
|
||||
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
||||
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
||||
export const getFormalSettlements = keyword =>
|
||||
request({ url: `${baseUrl}/candidate-formal-settlements`, method: 'get', params: { keyword } });
|
||||
export const getFormalDetails = formalSettlementId =>
|
||||
request({ url: `${baseUrl}/formal-details`, method: 'get', params: { formalSettlementId } });
|
||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
export const submit = id => request({ url: `${baseUrl}/submit`, method: 'post', data: { id } });
|
||||
export const approve = id => request({ url: `${baseUrl}/approve`, method: 'post', data: { id } });
|
||||
export const returnBill = (id, reason) =>
|
||||
request({ url: `${baseUrl}/return`, method: 'post', data: { id, reason } });
|
||||
export const repush = id => request({ url: `${baseUrl}/repush`, method: 'post', params: { id } });
|
||||
|
||||
export const approvalStatusOptions = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '审批中', value: 'reviewing' },
|
||||
{ label: '审批通过', value: 'approved' },
|
||||
{ label: '已驳回', value: 'returned' },
|
||||
];
|
||||
@@ -0,0 +1,37 @@
|
||||
import request from '@/axios';
|
||||
|
||||
const baseUrl = '/blade-transport/transport-reconciliation';
|
||||
|
||||
export const getList = (current, size, params) =>
|
||||
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
||||
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
||||
export const getFormalOptions = (current, size, params) =>
|
||||
request({
|
||||
url: `${baseUrl}/formal-options`,
|
||||
method: 'get',
|
||||
params: { current, size, ...params },
|
||||
});
|
||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
export const match = id => request({ url: `${baseUrl}/match`, method: 'post', params: { id } });
|
||||
export const manualMatch = data =>
|
||||
request({ url: `${baseUrl}/manual-match`, method: 'post', data });
|
||||
export const unmatch = internalId =>
|
||||
request({ url: `${baseUrl}/unmatch`, method: 'post', params: { internalId } });
|
||||
export const adjust = data => request({ url: `${baseUrl}/adjust`, method: 'post', data });
|
||||
export const updateByMatch = id =>
|
||||
request({ url: `${baseUrl}/update-by-match`, method: 'post', params: { id } });
|
||||
export const complete = id =>
|
||||
request({ url: `${baseUrl}/complete`, method: 'post', params: { id } });
|
||||
export const template = mode =>
|
||||
request({ url: `${baseUrl}/template`, method: 'get', params: { mode }, responseType: 'blob' });
|
||||
|
||||
const importFile = (url, id, file) => {
|
||||
const data = new FormData();
|
||||
data.append('id', id);
|
||||
data.append('file', file);
|
||||
return request({ url, method: 'post', data, responseType: 'blob', timeout: 60000 });
|
||||
};
|
||||
|
||||
export const importVehicle = (id, file) => importFile(`${baseUrl}/import-vehicle`, id, file);
|
||||
export const importCargo = (id, file) => importFile(`${baseUrl}/import-cargo`, id, file);
|
||||
@@ -78,3 +78,16 @@ export const recognitionTransportCertificates = certificates => {
|
||||
data: certificates,
|
||||
});
|
||||
};
|
||||
|
||||
export const recognizeBaiduOcr = (imageUrl, type, side = '') => {
|
||||
return request({
|
||||
url: '/blade-transport/baidu-ocr/recognize-url',
|
||||
method: 'post',
|
||||
timeout: 60000,
|
||||
params: {
|
||||
imageUrl,
|
||||
type,
|
||||
...(side ? { side } : {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -58,3 +58,16 @@ export const getExpiryStat = params => {
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
export const recognizeBaiduOcr = (imageUrl, type, side = '') => {
|
||||
return request({
|
||||
url: '/blade-transport/baidu-ocr/recognize-url',
|
||||
method: 'post',
|
||||
timeout: 60000,
|
||||
params: {
|
||||
imageUrl,
|
||||
type,
|
||||
...(side ? { side } : {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -75,3 +75,16 @@ export const recognitionTransportCertificates = objectKeys => {
|
||||
data: objectKeys,
|
||||
});
|
||||
};
|
||||
|
||||
export const recognizeBaiduOcr = (imageUrl, type, side = '') => {
|
||||
return request({
|
||||
url: '/blade-transport/baidu-ocr/recognize-url',
|
||||
method: 'post',
|
||||
timeout: 60000,
|
||||
params: {
|
||||
imageUrl,
|
||||
type,
|
||||
...(side ? { side } : {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -52,6 +52,9 @@ export const loginBySso = (tenantId, state, code) =>
|
||||
headers: {
|
||||
'Tenant-Id': tenantId,
|
||||
},
|
||||
meta: {
|
||||
isToken: false,
|
||||
},
|
||||
params: {
|
||||
tenant_id: tenantId,
|
||||
code,
|
||||
|
||||
@@ -112,3 +112,15 @@ export const getScoreTemplate = quantificationId => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const recognizeBusinessLicenseOcr = imageUrl => {
|
||||
return request({
|
||||
url: '/blade-transport/baidu-ocr/recognize-url',
|
||||
method: 'post',
|
||||
timeout: 60000,
|
||||
params: {
|
||||
imageUrl,
|
||||
type: 'business_license',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -47,3 +47,15 @@ export const update = row => {
|
||||
data: row,
|
||||
});
|
||||
};
|
||||
|
||||
export const recognizeGeneralOcr = imageUrl => {
|
||||
return request({
|
||||
url: '/blade-transport/baidu-ocr/recognize-url',
|
||||
method: 'post',
|
||||
timeout: 60000,
|
||||
params: {
|
||||
imageUrl,
|
||||
type: 'general',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -42,12 +42,18 @@ export const config = {
|
||||
batchDelete: false,
|
||||
enableContractForm: true,
|
||||
enableProjectSelect: true,
|
||||
projectQueryParams: {
|
||||
approvalStatuses: 'approved,change_approved',
|
||||
},
|
||||
enableContractPeriod: true,
|
||||
enableContractFileUpload: true,
|
||||
enableCurrentUserHandler: true,
|
||||
enableBillingPlan: true,
|
||||
enableSettlementRule: true,
|
||||
enableReconciliation: true,
|
||||
enableReconciliation: false,
|
||||
enableFeeGenerationMode: true,
|
||||
enableSettlementConfigTabs: true,
|
||||
enablePaymentRatio: true,
|
||||
enableAttachmentTable: true,
|
||||
enableChangeRecord: true,
|
||||
enableOrganizationSelect: true,
|
||||
@@ -57,6 +63,53 @@ export const config = {
|
||||
statusProp: 'approvalStatus',
|
||||
statusTextProp: 'approvalStatusName',
|
||||
customMenuActions: true,
|
||||
contractNameDetail: true,
|
||||
detailSections: [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
['contractNo', '合同编号'],
|
||||
['contractName', '合同名称'],
|
||||
['projectName', '所属项目'],
|
||||
['organizationName', '所属组织'],
|
||||
['contractCategory', '合同类别'],
|
||||
['signType', '签约类型'],
|
||||
['partyA', '甲方'],
|
||||
['partyB', '乙方'],
|
||||
['handlerUserName', '经办人'],
|
||||
['signDate', '签订日期'],
|
||||
['contractFormat', '合同格式'],
|
||||
['legalSealFlag', '是否需要加盖法人章'],
|
||||
['copyCount', '一式'],
|
||||
['paymentDays', '回款账期'],
|
||||
['remark', '备注', 2],
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '合同期限',
|
||||
fields: [
|
||||
['startDate', '开始日期'],
|
||||
['endDate', '结束日期'],
|
||||
['temporaryStartDate', '临时效力起'],
|
||||
['temporaryEndDate', '临时效力止'],
|
||||
['effectiveType', '生效类型'],
|
||||
['contractStage', '合同阶段'],
|
||||
['approvalStatus', '审核状态'],
|
||||
['currentNode', '当前节点'],
|
||||
['currentProcessor', '当前处理人'],
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '计费与结算',
|
||||
fields: [
|
||||
['feeGenerationMode', '费用生成模式'],
|
||||
['billingPlanJson', '计费方案', 2],
|
||||
['settlementMode', '结算类型'],
|
||||
['settlementRuleJson', '结算规则', 2],
|
||||
['paymentRatioJson', '付款比例设置', 2],
|
||||
],
|
||||
},
|
||||
],
|
||||
editLabelStatusMap: {
|
||||
change_rejected: '编辑变更内容',
|
||||
},
|
||||
@@ -96,6 +149,22 @@ export const config = {
|
||||
stage: ['temporary', 'formal'],
|
||||
permission: 'contract_manage_reject',
|
||||
},
|
||||
{
|
||||
action: 'approve',
|
||||
label: '通过',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['change_reviewing'],
|
||||
stage: ['formal'],
|
||||
permission: 'contract_manage_approve',
|
||||
},
|
||||
{
|
||||
action: 'reject',
|
||||
label: '驳回',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['change_reviewing'],
|
||||
stage: ['formal'],
|
||||
permission: 'contract_manage_reject',
|
||||
},
|
||||
{
|
||||
action: 'submitFormal',
|
||||
label: '重新提交转正',
|
||||
@@ -186,6 +255,7 @@ export const option = createCrudOption([
|
||||
searchOrder: 7,
|
||||
searchPlaceholder: '请输入',
|
||||
order: 180,
|
||||
slot: true,
|
||||
minWidth: 180,
|
||||
rules: textRule('合同名称', 100, true),
|
||||
},
|
||||
@@ -198,7 +268,8 @@ export const option = createCrudOption([
|
||||
searchPlaceholder: '请选择',
|
||||
formslot: true,
|
||||
order: 140,
|
||||
dicUrl: '/blade-transport/project-apply/list?current=1&size=9999',
|
||||
dicUrl:
|
||||
'/blade-transport/project-apply/list?current=1&size=9999&approvalStatuses=approved,change_approved',
|
||||
dicFormatter: listDicFormatter,
|
||||
props: {
|
||||
label: 'projectName',
|
||||
@@ -481,7 +552,7 @@ export const option = createCrudOption([
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '计费信息',
|
||||
label: '费用生成模式',
|
||||
prop: 'billingEnabled',
|
||||
type: 'switch',
|
||||
dicData: [
|
||||
@@ -515,10 +586,11 @@ export const option = createCrudOption([
|
||||
labelWidth: 0,
|
||||
},
|
||||
{
|
||||
label: '附件',
|
||||
label: '',
|
||||
prop: 'contractFileJson',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
className: 'contract-file-form-item',
|
||||
order: 20,
|
||||
hide: true,
|
||||
},
|
||||
@@ -560,7 +632,7 @@ export const option = createCrudOption([
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
prop: 'reconciliationTitle',
|
||||
prop: 'paymentRatioTitle',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
order: -30,
|
||||
@@ -569,7 +641,7 @@ export const option = createCrudOption([
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
prop: 'reconciliationJson',
|
||||
prop: 'paymentRatioJson',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
order: -40,
|
||||
|
||||
@@ -63,15 +63,6 @@ const parseProcessNodes = value => {
|
||||
return Array.isArray(value.nodes) ? value.nodes : [];
|
||||
};
|
||||
|
||||
const requiresDriverAcceptConfirmation = processJson =>
|
||||
parseProcessNodes(processJson).some(
|
||||
node =>
|
||||
(node.key === 'accept' || node.name === '接单') &&
|
||||
node.enabled !== false &&
|
||||
node.confirmMode === 'yes' &&
|
||||
node.confirmDriver === true
|
||||
);
|
||||
|
||||
const getProcessConfigNodes = row => {
|
||||
const sources = [
|
||||
row.processJson,
|
||||
@@ -99,11 +90,79 @@ const getProcessConfigNodes = row => {
|
||||
});
|
||||
};
|
||||
|
||||
const hasAcceptProcessNode = row =>
|
||||
const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单';
|
||||
|
||||
const requiresDriverAcceptConfirmation = row =>
|
||||
getProcessConfigNodes(row).some(
|
||||
node => node.enabled !== false && (node.key === 'accept' || node.name === '接单')
|
||||
node =>
|
||||
isAcceptProcessNode(node) &&
|
||||
node.enabled !== false &&
|
||||
node.confirmMode === 'yes' &&
|
||||
node.confirmDriver === true
|
||||
);
|
||||
|
||||
const isDriverRejectedStatus = value => {
|
||||
if (value === null || value === undefined || typeof value === 'boolean') return false;
|
||||
return [
|
||||
'-1',
|
||||
'2',
|
||||
'reject',
|
||||
'rejected',
|
||||
'refuse',
|
||||
'refused',
|
||||
'decline',
|
||||
'declined',
|
||||
'deny',
|
||||
'denied',
|
||||
'拒绝',
|
||||
'已拒绝',
|
||||
'拒绝接单',
|
||||
'已拒绝接单',
|
||||
].includes(String(value).trim().toLowerCase());
|
||||
};
|
||||
|
||||
const hasDriverRejectRecord = row => {
|
||||
const rejectRecordValues = [
|
||||
row.driverRejectTime,
|
||||
row.driverRejectedTime,
|
||||
row.driverRefuseTime,
|
||||
row.acceptRejectTime,
|
||||
row.driverRejectReason,
|
||||
row.driverRefuseReason,
|
||||
row.acceptRejectReason,
|
||||
];
|
||||
if (rejectRecordValues.some(value => !isEmpty(value))) return true;
|
||||
|
||||
const driverStatusProps = [
|
||||
'driverAcceptStatus',
|
||||
'driverAccepted',
|
||||
'driverAccept',
|
||||
'accepted',
|
||||
'isAccepted',
|
||||
'acceptStatus',
|
||||
'driverResponseStatus',
|
||||
'driverOrderStatus',
|
||||
'driverAcceptResult',
|
||||
'acceptResult',
|
||||
];
|
||||
if (driverStatusProps.some(prop => isDriverRejectedStatus(row[prop]))) return true;
|
||||
|
||||
const nodeStatusProps = [
|
||||
...driverStatusProps,
|
||||
'confirmStatus',
|
||||
'executionStatus',
|
||||
'nodeStatus',
|
||||
'status',
|
||||
'result',
|
||||
];
|
||||
return getProcessConfigNodes(row)
|
||||
.filter(isAcceptProcessNode)
|
||||
.some(node => nodeStatusProps.some(prop => isDriverRejectedStatus(node[prop])));
|
||||
};
|
||||
|
||||
const isDriverRejectedWaybill = row =>
|
||||
requiresDriverAcceptConfirmation(row) && hasDriverRejectRecord(row);
|
||||
|
||||
const hasDriverAcceptRecord = row => {
|
||||
const recordValues = [
|
||||
row.driverAcceptTime,
|
||||
@@ -304,7 +363,7 @@ export const config = {
|
||||
formatStatus(row, prop, defaultText) {
|
||||
if (
|
||||
prop === 'businessStatus' &&
|
||||
hasAcceptProcessNode(row) &&
|
||||
requiresDriverAcceptConfirmation(row) &&
|
||||
isInProgressStatus(row, defaultText) &&
|
||||
!hasDriverAcceptRecord(row)
|
||||
) {
|
||||
@@ -313,12 +372,18 @@ export const config = {
|
||||
if (
|
||||
prop === 'businessStatus' &&
|
||||
row.businessStatus === 'pending' &&
|
||||
!requiresDriverAcceptConfirmation(row.processJson)
|
||||
!requiresDriverAcceptConfirmation(row)
|
||||
) {
|
||||
return '进行中';
|
||||
}
|
||||
return defaultText;
|
||||
},
|
||||
canReassign(row) {
|
||||
return isDriverRejectedWaybill(row);
|
||||
},
|
||||
canEdit(row) {
|
||||
return !isDriverRejectedWaybill(row);
|
||||
},
|
||||
deleteStatus: ['draft'],
|
||||
editStatus: ['draft', 'pending'],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
export const formalSettlementFormFields = [
|
||||
{ label: '结算单号', prop: 'formalSettlementNo', readonly: true },
|
||||
{ label: '项目名称', prop: 'projectName', readonly: true },
|
||||
{ label: '所属组织', prop: 'deptName', readonly: true },
|
||||
{ label: '合同编号', prop: 'contractNo', readonly: true },
|
||||
{ label: '合同名称', prop: 'contractId', type: 'contract', required: true },
|
||||
{ label: '付款方', prop: 'payerName', readonly: true },
|
||||
{ label: '收款方', prop: 'payeeName', readonly: true },
|
||||
{ label: '结算类型', prop: 'settlementTypeName', readonly: true },
|
||||
{ label: '结算金额', prop: 'settlementAmount', readonly: true, money: true },
|
||||
{ label: '汇率日期', prop: 'exchangeRateDate', type: 'date', required: true },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', type: 'number', required: true },
|
||||
{ label: '本位币合计', prop: 'localSettlementAmount', readonly: true, money: true },
|
||||
{ label: '申请付款金额(含预付)', prop: 'appliedPaymentAmount', readonly: true, money: true },
|
||||
{ label: '已收/已付合计', prop: 'paidAmount', readonly: true, money: true },
|
||||
];
|
||||
|
||||
export const createFormalSettlementForm = () => ({
|
||||
id: null,
|
||||
formalSettlementNo: '',
|
||||
contractId: null,
|
||||
contractName: '',
|
||||
settlementType: '',
|
||||
sourcePreSettlementIds: [],
|
||||
sourceDetailIds: [],
|
||||
exchangeRateDate: '',
|
||||
exchangeRate: 1,
|
||||
attachmentsJson: '[]',
|
||||
remark: '',
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
export const approvalStatusOptions = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '审批中', value: 'reviewing' },
|
||||
{ label: '审批通过', value: 'approved' },
|
||||
{ label: '已驳回', value: 'returned' },
|
||||
{ label: '已作废', value: 'voided' },
|
||||
];
|
||||
|
||||
export const invoiceStatusOptions = [
|
||||
{ label: '未收/开票', value: 'unreceived' },
|
||||
{ label: '部分收/开票', value: 'partial' },
|
||||
{ label: '已收/开票', value: 'completed' },
|
||||
];
|
||||
|
||||
export const paymentStatusOptions = [
|
||||
{ label: '未收/付款', value: 'unpaid' },
|
||||
{ label: '部分收/付款', value: 'partial' },
|
||||
{ label: '已收/付款', value: 'paid' },
|
||||
];
|
||||
|
||||
export const kingdeeSyncStatusOptions = [
|
||||
{ label: '未同步', value: 'unsynced' },
|
||||
{ label: '已同步', value: 'synced' },
|
||||
{ label: '同步失败', value: 'failed' },
|
||||
];
|
||||
|
||||
export const formalSettlementSearchFields = [
|
||||
{ label: '结算单号', prop: 'formalSettlementNo', type: 'input' },
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', type: 'input' },
|
||||
{ label: '项目', prop: 'projectName', type: 'input' },
|
||||
{ label: '所属组织', prop: 'deptName', type: 'input' },
|
||||
{ label: '合同编号', prop: 'contractNo', type: 'input' },
|
||||
{ label: '付款方', prop: 'payerName', type: 'input' },
|
||||
{ label: '收款方', prop: 'payeeName', type: 'input' },
|
||||
{ label: '发票状态', prop: 'invoiceStatus', type: 'select', options: invoiceStatusOptions },
|
||||
{ label: '收付款状态', prop: 'paymentStatus', type: 'select', options: paymentStatusOptions },
|
||||
{ label: '审核状态', prop: 'approvalStatus', type: 'select', options: approvalStatusOptions },
|
||||
{
|
||||
label: '金蝶单据状态',
|
||||
prop: 'kingdeeSyncStatus',
|
||||
type: 'select',
|
||||
options: kingdeeSyncStatusOptions,
|
||||
},
|
||||
{ label: '生成日期', prop: 'createDateRange', type: 'daterange' },
|
||||
];
|
||||
@@ -0,0 +1,94 @@
|
||||
export const formalSettlementTableColumns = [
|
||||
{ label: '结算单号', prop: 'formalSettlementNo', minWidth: 160, link: true, fixed: 'left' },
|
||||
{ label: '预结算单号', prop: 'preSettlementNos', minWidth: 190 },
|
||||
{ label: '来源', prop: 'sourceType', minWidth: 110 },
|
||||
{ label: '付款方', prop: 'payerName', minWidth: 150 },
|
||||
{ label: '收款方', prop: 'payeeName', minWidth: 150 },
|
||||
{ label: '项目名称', prop: 'projectName', minWidth: 140 },
|
||||
{ label: '所属组织', prop: 'deptName', minWidth: 140 },
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 150 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 170 },
|
||||
{ label: '原币结算金额', prop: 'settlementAmount', minWidth: 145, money: true },
|
||||
{ label: '本位币结算金额', prop: 'localSettlementAmount', minWidth: 155, money: true },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', minWidth: 110 },
|
||||
{ label: '申请付款金额(含预付)', prop: 'appliedPaymentAmount', minWidth: 185, money: true },
|
||||
{ label: '已收/已付合计', prop: 'paidAmount', minWidth: 145, money: true },
|
||||
{ label: '发票状态', prop: 'invoiceStatusName', minWidth: 120 },
|
||||
{ label: '收付款状态', prop: 'paymentStatusName', minWidth: 120 },
|
||||
{ label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true },
|
||||
{ label: '当前节点', prop: 'currentNode', minWidth: 130 },
|
||||
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
|
||||
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
|
||||
{ label: '创建人', prop: 'createUserName', minWidth: 110 },
|
||||
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
|
||||
];
|
||||
|
||||
export const sourceColumns = [
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', minWidth: 160 },
|
||||
{ label: '结算金额', prop: 'settlementAmount', minWidth: 130, money: true },
|
||||
{ label: '申请预付金额', prop: 'advanceAppliedAmount', minWidth: 140, money: true },
|
||||
{ label: '已付款金额', prop: 'advancePaidAmount', minWidth: 130, money: true },
|
||||
];
|
||||
|
||||
export const detailColumns = [
|
||||
{ label: '单据号', prop: 'documentNo', minWidth: 180 },
|
||||
{ label: '运单号', prop: 'waybillNo', minWidth: 160 },
|
||||
{ label: '车号', prop: 'vehicleNo', minWidth: 130 },
|
||||
{ label: '发货地址', prop: 'departureAddress', minWidth: 180 },
|
||||
{ label: '到货地址', prop: 'arrivalAddress', minWidth: 180 },
|
||||
{ label: '实际发货时间', prop: 'actualDepartureTime', minWidth: 170 },
|
||||
{ label: '实际完成时间', prop: 'actualCompletionTime', minWidth: 170 },
|
||||
{ label: '运输类型', prop: 'transportType', minWidth: 120 },
|
||||
{ label: '货物名称', prop: 'cargoName', minWidth: 140 },
|
||||
{ label: '货物类型', prop: 'cargoType', minWidth: 130 },
|
||||
{ label: '运输总量', prop: 'transportQuantity', minWidth: 120 },
|
||||
{ label: '里程(KM)', prop: 'mileage', minWidth: 120 },
|
||||
{ label: '批次号', prop: 'batchNo', minWidth: 120 },
|
||||
{ label: '运输单价', prop: 'unitPrice', minWidth: 120, money: true },
|
||||
{ label: '运费', prop: 'freightAmount', minWidth: 120, money: true },
|
||||
{ label: '调整金额', prop: 'adjustAmount', minWidth: 120, money: true },
|
||||
{ label: '结算金额(含税)', prop: 'settlementAmountTax', minWidth: 155, money: true },
|
||||
{ label: '结算金额(不含税)', prop: 'settlementAmountNoTax', minWidth: 170, money: true },
|
||||
{ label: '备注', prop: 'remark', minWidth: 180 },
|
||||
];
|
||||
|
||||
export const candidateColumns = [
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', minWidth: 170 },
|
||||
{ label: '项目名称', prop: 'projectName', minWidth: 140 },
|
||||
{ label: '所属组织', prop: 'deptName', minWidth: 140 },
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 150 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 170 },
|
||||
{ label: '付款方', prop: 'payerName', minWidth: 150 },
|
||||
{ label: '收款方', prop: 'payeeName', minWidth: 150 },
|
||||
{ label: '结算金额', prop: 'settlementAmount', minWidth: 130, money: true },
|
||||
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
|
||||
];
|
||||
|
||||
export const candidateDetailColumns = [
|
||||
{ label: '单据号', prop: 'documentNo', minWidth: 180 },
|
||||
{ label: '项目名称', prop: 'projectName', minWidth: 140 },
|
||||
{ label: '所属组织', prop: 'deptName', minWidth: 140 },
|
||||
{ label: '生成日期', prop: 'feeDate', minWidth: 120 },
|
||||
{ label: '客商名称', prop: 'customerName', minWidth: 150 },
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 150 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 170 },
|
||||
{ label: '来源', prop: 'sourceType', minWidth: 110 },
|
||||
{ label: '运单号', prop: 'waybillNo', minWidth: 150 },
|
||||
{ label: '车号', prop: 'vehicleNo', minWidth: 130 },
|
||||
{ label: '运输类型', prop: 'transportType', minWidth: 120 },
|
||||
{ label: '货物名称', prop: 'cargoName', minWidth: 140 },
|
||||
{ label: '货物类型', prop: 'cargoType', minWidth: 130 },
|
||||
{ label: '批次号', prop: 'batchNo', minWidth: 120 },
|
||||
{ label: '应收应付合计', prop: 'totalAmount', minWidth: 140, money: true },
|
||||
{ label: '状态', prop: 'settlementStatusName', minWidth: 110 },
|
||||
];
|
||||
|
||||
export const paymentColumns = [
|
||||
{ label: '付款类型', prop: 'paymentTypeName', minWidth: 120 },
|
||||
{ label: '单据号', prop: 'paymentNo', minWidth: 160 },
|
||||
{ label: '申请付款金额', prop: 'appliedAmount', minWidth: 145, money: true },
|
||||
{ label: '已付款金额', prop: 'paidAmount', minWidth: 130, money: true },
|
||||
{ label: '单据状态', prop: 'billStatusName', minWidth: 120 },
|
||||
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
|
||||
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
|
||||
];
|
||||
@@ -0,0 +1,79 @@
|
||||
export const settlementTypeOptions = [
|
||||
{ label: '应收', value: 'receivable' },
|
||||
{ label: '应付', value: 'payable' },
|
||||
];
|
||||
|
||||
export const preSettlementFormFields = [
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', readonly: true },
|
||||
{ label: '合同名称', prop: 'contractId', type: 'contract', required: true },
|
||||
{ label: '合同编号', prop: 'contractNo', readonly: true },
|
||||
{ label: '项目名称', prop: 'projectName', readonly: true },
|
||||
{ label: '收款方', prop: 'payeeName', readonly: true },
|
||||
{ label: '付款方', prop: 'payerName', readonly: true },
|
||||
{ label: '结算类型', prop: 'settlementType', readonly: true },
|
||||
{ label: '结算金额', prop: 'settlementAmount', readonly: true, money: true },
|
||||
{ label: '汇率日期', prop: 'exchangeRateDate', type: 'date' },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', type: 'number' },
|
||||
{ label: '本位币合计', prop: 'localSettlementAmount', readonly: true, money: true },
|
||||
{ label: '创建人', prop: 'createUserName', readonly: true },
|
||||
{ label: '创建日期', prop: 'createTime', readonly: true },
|
||||
{ label: '备注', prop: 'remark', type: 'textarea', span: 24, maxlength: 200 },
|
||||
];
|
||||
|
||||
export const preSettlementFormRules = {
|
||||
contractId: [{ required: true, message: '请选择合同名称', trigger: 'change' }],
|
||||
exchangeRateDate: [
|
||||
{
|
||||
validator: (_rule, value, callback, source) => {
|
||||
if (source.currency && source.currency !== 'RMB' && !value) {
|
||||
callback(new Error('外币结算请选择汇率日期'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
exchangeRate: [
|
||||
{
|
||||
validator: (_rule, value, callback, source) => {
|
||||
if (source.currency && source.currency !== 'RMB' && (!value || Number(value) <= 0)) {
|
||||
callback(new Error('外币结算请输入大于0的结算汇率'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
remark: [{ max: 200, message: '备注不能超过200个字符', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
export const emptyPreSettlementForm = () => ({
|
||||
id: '',
|
||||
preSettlementNo: '',
|
||||
contractId: '',
|
||||
contractNo: '',
|
||||
contractName: '',
|
||||
projectId: '',
|
||||
projectName: '',
|
||||
deptId: '',
|
||||
deptName: '',
|
||||
payeeName: '',
|
||||
payerName: '',
|
||||
settlementType: 'payable',
|
||||
currency: 'RMB',
|
||||
settlementAmount: 0,
|
||||
localCurrency: 'RMB',
|
||||
localSettlementAmount: 0,
|
||||
exchangeRateDate: '',
|
||||
exchangeRate: 1,
|
||||
approvalStatus: 'draft',
|
||||
approvalStatusName: '草稿',
|
||||
currentNode: '草稿',
|
||||
currentProcessor: '',
|
||||
createUserName: '',
|
||||
createTime: '',
|
||||
attachmentsJson: '[]',
|
||||
remark: '',
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
export const approvalStatusOptions = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '审批中', value: 'reviewing' },
|
||||
{ label: '审批通过', value: 'approved' },
|
||||
{ label: '已驳回', value: 'returned' },
|
||||
{ label: '已作废', value: 'voided' },
|
||||
];
|
||||
|
||||
export const preSettlementSearchFields = [
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', type: 'input' },
|
||||
{ label: '预付单号', prop: 'advanceNo', type: 'input' },
|
||||
{ label: '项目名称', prop: 'projectName', type: 'input' },
|
||||
{ label: '所属组织', prop: 'deptName', type: 'input' },
|
||||
{ label: '合同名称', prop: 'contractName', type: 'input' },
|
||||
{ label: '合同编号', prop: 'contractNo', type: 'input' },
|
||||
{ label: '收款方', prop: 'payeeName', type: 'input' },
|
||||
{ label: '付款方', prop: 'payerName', type: 'input' },
|
||||
{ label: '生成日期', prop: 'createDateRange', type: 'daterange' },
|
||||
{
|
||||
label: '审核状态',
|
||||
prop: 'approvalStatus',
|
||||
type: 'select',
|
||||
options: approvalStatusOptions,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,88 @@
|
||||
export const preSettlementTableColumns = [
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', minWidth: 150, link: true, fixed: 'left' },
|
||||
{ label: '来源', prop: 'sourceType', minWidth: 100 },
|
||||
{ label: '付款方', prop: 'payerName', minWidth: 160 },
|
||||
{ label: '收款方', prop: 'payeeName', minWidth: 160 },
|
||||
{ label: '项目名称', prop: 'projectName', minWidth: 130 },
|
||||
{ label: '所属组织', prop: 'deptName', minWidth: 140 },
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 140 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 160 },
|
||||
{ label: '原币结算金额', prop: 'settlementAmount', minWidth: 140, money: true, precision: 2 },
|
||||
{ label: '本位币结算金额', prop: 'localSettlementAmount', minWidth: 150, money: true, precision: 2 },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', minWidth: 110, precision: 2 },
|
||||
{ label: '申请预付金额', prop: 'advanceAppliedAmount', minWidth: 140, money: true },
|
||||
{ label: '已付款金额', prop: 'advancePaidAmount', minWidth: 130, money: true },
|
||||
{ label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true },
|
||||
{ label: '当前节点', prop: 'currentNode', minWidth: 130 },
|
||||
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
|
||||
{ label: '创建人', prop: 'createUserName', minWidth: 110 },
|
||||
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
|
||||
];
|
||||
|
||||
export const settlementSummaryColumns = [
|
||||
{ label: '费用类型', prop: 'feeType', minWidth: 150 },
|
||||
{ label: '费用项', prop: 'feeItem', minWidth: 160 },
|
||||
{ label: '原金额', prop: 'originalAmount', minWidth: 130 },
|
||||
{ label: '调整金额', prop: 'adjustAmount', minWidth: 140 },
|
||||
{ label: '结算金额', prop: 'settlementAmount', minWidth: 140 },
|
||||
{ label: '备注', prop: 'remark', minWidth: 200 },
|
||||
];
|
||||
|
||||
export const settlementDetailColumns = [
|
||||
{ label: '单据号', prop: 'documentNo', minWidth: 180, link: true },
|
||||
{ label: '运单号', prop: 'waybillNo', minWidth: 160 },
|
||||
{ label: '车号', prop: 'vehicleNo', minWidth: 130 },
|
||||
{ label: '发货地址', prop: 'departureAddress', minWidth: 180 },
|
||||
{ label: '到货地址', prop: 'arrivalAddress', minWidth: 180 },
|
||||
{ label: '实际发货时间', prop: 'actualDepartureTime', minWidth: 170 },
|
||||
{ label: '实际完成时间', prop: 'actualCompletionTime', minWidth: 170 },
|
||||
{ label: '运输类型', prop: 'transportType', minWidth: 120 },
|
||||
{ label: '货物名称', prop: 'cargoName', minWidth: 150 },
|
||||
{ label: '货物类型', prop: 'cargoType', minWidth: 140 },
|
||||
{ label: '运输总量', prop: 'transportQuantityText', minWidth: 120 },
|
||||
{ label: '里程(KM)', prop: 'mileage', minWidth: 120 },
|
||||
{ label: '批次号', prop: 'batchNo', minWidth: 130 },
|
||||
{ label: '运输单价', prop: 'unitPrice', minWidth: 120, money: true },
|
||||
{ label: '运费', prop: 'freightAmount', minWidth: 120, money: true },
|
||||
{ label: '原金额', prop: 'originalAmount', minWidth: 120, money: true },
|
||||
{ label: '调整金额', prop: 'adjustAmount', minWidth: 120, money: true },
|
||||
{ label: '结算金额(含税)', prop: 'settlementAmountTax', minWidth: 150, money: true },
|
||||
{ label: '结算金额(不含税)', prop: 'settlementAmountNoTax', minWidth: 165, money: true },
|
||||
{ label: '备注', prop: 'remark', minWidth: 180 },
|
||||
];
|
||||
|
||||
export const advanceColumns = [
|
||||
{ label: '预付单号', prop: 'advanceNo', minWidth: 160 },
|
||||
{ label: '申请预付金额', prop: 'appliedAmount', minWidth: 150 },
|
||||
{ label: '已付款金额', prop: 'paidAmount', minWidth: 140 },
|
||||
{ label: '单据状态', prop: 'billStatus', minWidth: 120 },
|
||||
{ label: '金蝶预付单号', prop: 'kingdeeAdvanceNo', minWidth: 170 },
|
||||
{ label: '创建人', prop: 'createUserName', minWidth: 120 },
|
||||
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
|
||||
];
|
||||
|
||||
export const changeRecordColumns = [
|
||||
{ label: '变更类型', prop: 'changeType', minWidth: 130 },
|
||||
{ label: '行号', prop: 'lineNo', minWidth: 80 },
|
||||
{ label: '类型', prop: 'operationType', minWidth: 100 },
|
||||
{ label: '变更内容', prop: 'changeContent', minWidth: 320 },
|
||||
{ label: '操作人', prop: 'operatorName', minWidth: 120 },
|
||||
{ label: '变更原因', prop: 'changeReason', minWidth: 180 },
|
||||
{ label: '变更时间', prop: 'changeTime', minWidth: 170 },
|
||||
];
|
||||
|
||||
export const candidateDetailColumns = [
|
||||
{ label: '单据号', prop: 'documentNo', minWidth: 180 },
|
||||
{ label: '费用日期', prop: 'feeDate', minWidth: 120 },
|
||||
{ label: '客商名称', prop: 'customerName', minWidth: 150 },
|
||||
{ label: '发货地点', prop: 'departureAddress', minWidth: 170 },
|
||||
{ label: '到货地点', prop: 'arrivalAddress', minWidth: 170 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 160 },
|
||||
{ label: '运单号', prop: 'waybillNo', minWidth: 150 },
|
||||
{ label: '车号', prop: 'vehicleNo', minWidth: 130 },
|
||||
{ label: '运输类型', prop: 'transportType', minWidth: 120 },
|
||||
{ label: '货物名称', prop: 'cargoName', minWidth: 140 },
|
||||
{ label: '货物类型', prop: 'cargoType', minWidth: 130 },
|
||||
{ label: '批次号', prop: 'batchNo', minWidth: 120 },
|
||||
{ label: '结算金额', prop: 'totalAmount', minWidth: 130, money: true },
|
||||
];
|
||||
@@ -46,8 +46,6 @@ export const tableColumns = [
|
||||
{ label: '里程(KM)', prop: 'mileage', minWidth: 120, align: 'right' },
|
||||
{ label: '批次号', prop: 'batchNo', minWidth: 120 },
|
||||
{ label: '运输单价', prop: 'unitPriceText', minWidth: 120, align: 'right' },
|
||||
{ label: '运输费', prop: 'freightAmountText', minWidth: 120, align: 'right' },
|
||||
{ label: '其它费用', prop: 'otherFeeAmountText', minWidth: 120, align: 'right' },
|
||||
{ label: '费用合计', prop: 'totalAmountText', minWidth: 130, align: 'right' },
|
||||
{ label: '状态', prop: 'settlementStatusName', minWidth: 120 },
|
||||
{ label: '创建人', prop: 'createUserName', minWidth: 120 },
|
||||
@@ -114,6 +112,23 @@ export const generateWaybillColumns = [
|
||||
{ label: '运输类型', prop: 'transportType', minWidth: 130 },
|
||||
{ label: '承运类型', prop: 'carrierType', minWidth: 130 },
|
||||
{ label: '货物信息', prop: 'cargoInfo', minWidth: 160 },
|
||||
{ label: '发货地址', prop: 'departureAddress', minWidth: 220 },
|
||||
{ label: '发货联系人', prop: 'departureContact', minWidth: 130 },
|
||||
{ label: '到货地址', prop: 'arrivalAddress', minWidth: 220 },
|
||||
{ label: '收货联系人', prop: 'arrivalContact', minWidth: 130 },
|
||||
{ label: '单价', prop: 'unitPrice', minWidth: 110, align: 'right' },
|
||||
{ label: '运费', prop: 'freight', minWidth: 110, align: 'right' },
|
||||
{ label: '其他费用合计', prop: 'otherFeeTotal', minWidth: 140, align: 'right' },
|
||||
{ label: '运费合计', prop: 'freightTotal', minWidth: 120, align: 'right' },
|
||||
{ label: '客户合同', prop: 'contractName', minWidth: 160 },
|
||||
{ label: '发货计划', prop: 'planName', minWidth: 140 },
|
||||
{ label: '运输批次', prop: 'batchNo', minWidth: 140 },
|
||||
{ label: '原始单号', prop: 'originalNo', minWidth: 140 },
|
||||
{ label: '多式联运总单号', prop: 'masterNo', minWidth: 160 },
|
||||
{ label: '备注', prop: 'remark', minWidth: 180 },
|
||||
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
|
||||
{ label: '更新时间', prop: 'updateTime', minWidth: 170 },
|
||||
{ label: '状态', prop: 'businessStatusName', minWidth: 110 },
|
||||
];
|
||||
|
||||
export const generatePreviewColumns = [
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
export const settlementAdjustmentFormFields = [
|
||||
{ label: '单据号', prop: 'adjustmentNo' },
|
||||
{ label: '* 关联结算单号', prop: 'formalSettlementId' },
|
||||
{ label: '客户/客商名称', prop: 'customerName' },
|
||||
{ label: '结算单类型', prop: 'settlementTypeName' },
|
||||
{ label: '关联合同名称', prop: 'contractName' },
|
||||
{ label: '关联项目', prop: 'projectName' },
|
||||
{ label: '原结算金额', prop: 'originalSettlementAmount', money: true },
|
||||
{ label: '调整金额', prop: 'adjustmentAmount', money: true },
|
||||
{ label: '调整后结算金额', prop: 'adjustedSettlementAmount', money: true },
|
||||
];
|
||||
|
||||
export const createSettlementAdjustmentForm = () => ({
|
||||
id: null,
|
||||
adjustmentNo: '',
|
||||
formalSettlementId: null,
|
||||
formalSettlementNo: '',
|
||||
settlementType: '',
|
||||
projectName: '',
|
||||
deptName: '',
|
||||
customerName: '',
|
||||
contractNo: '',
|
||||
contractName: '',
|
||||
adjustmentAmount: 0,
|
||||
originalSettlementAmount: 0,
|
||||
adjustedSettlementAmount: 0,
|
||||
remark: '',
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { approvalStatusOptions } from '@/api/settlement/settlementAdjustment';
|
||||
|
||||
export const settlementAdjustmentSearchFields = [
|
||||
{ label: '单据号', prop: 'adjustmentNo', type: 'input' },
|
||||
{ label: '日期', prop: 'createDateRange', type: 'daterange' },
|
||||
{ label: '客户名称', prop: 'customerName', type: 'input' },
|
||||
{ label: '项目名称', prop: 'projectName', type: 'input' },
|
||||
{ label: '所属组织', prop: 'deptName', type: 'input' },
|
||||
{ label: '关联结算单', prop: 'formalSettlementNo', type: 'input' },
|
||||
{
|
||||
label: '结算单类型',
|
||||
prop: 'settlementType',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '应付', value: 'payable' },
|
||||
{ label: '应收', value: 'receivable' },
|
||||
],
|
||||
},
|
||||
{ label: '审核状态', prop: 'approvalStatus', type: 'select', options: approvalStatusOptions },
|
||||
];
|
||||
@@ -0,0 +1,18 @@
|
||||
export const settlementAdjustmentTableColumns = [
|
||||
{ label: '单据号', prop: 'adjustmentNo', minWidth: 160, link: true, fixed: 'left' },
|
||||
{ label: '结算单号', prop: 'formalSettlementNo', minWidth: 160 },
|
||||
{ label: '结算单类型', prop: 'settlementTypeName', minWidth: 110 },
|
||||
{ label: '项目名称', prop: 'projectName', minWidth: 140 },
|
||||
{ label: '所属组织', prop: 'deptName', minWidth: 140 },
|
||||
{ label: '客商名称', prop: 'customerName', minWidth: 150 },
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 150 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 170 },
|
||||
{ label: '调整金额', prop: 'adjustmentAmount', minWidth: 130, money: true },
|
||||
{ label: '原结算金额', prop: 'originalSettlementAmount', minWidth: 140, money: true },
|
||||
{ label: '调整后结算金额', prop: 'adjustedSettlementAmount', minWidth: 160, money: true },
|
||||
{ label: '审核状态', prop: 'approvalStatusName', minWidth: 110, status: true },
|
||||
{ label: '当前节点', prop: 'currentNode', minWidth: 120 },
|
||||
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
|
||||
{ label: '创建人', prop: 'createUserName', minWidth: 110 },
|
||||
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
|
||||
];
|
||||
@@ -0,0 +1,13 @@
|
||||
export const transportReconciliationFormFields = [
|
||||
{ prop: 'reconciliationNo', label: '对账单号', readonly: true },
|
||||
{ prop: 'reconciliationMode', label: '对账模式', type: 'mode' },
|
||||
{ prop: 'formalSettlementNo', label: '正式结算单', type: 'formal' },
|
||||
{ prop: 'payerName', label: '付款方', readonly: true },
|
||||
{ prop: 'payeeName', label: '收款方', readonly: true },
|
||||
{ prop: 'projectName', label: '项目', readonly: true },
|
||||
{ prop: 'deptName', label: '所属组织', readonly: true },
|
||||
{ prop: 'contractName', label: '合同名称', readonly: true },
|
||||
{ prop: 'paidAmount', label: '已付合计', money: true, readonly: true },
|
||||
{ prop: 'reconcilerName', label: '对账人', readonly: true },
|
||||
{ prop: 'reconciliationDate', label: '对账日期', type: 'date' },
|
||||
];
|
||||
@@ -0,0 +1,34 @@
|
||||
export const reconciliationMatchStatusOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '未匹配', value: 'unmatched' },
|
||||
{ label: '部分匹配', value: 'partial' },
|
||||
{ label: '已匹配', value: 'matched' },
|
||||
];
|
||||
|
||||
export const reconciliationStatusOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '未完成', value: 'unfinished' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
];
|
||||
|
||||
export const transportReconciliationSearchFields = [
|
||||
{ prop: 'reconciliationNo', label: '对账单号', type: 'input' },
|
||||
{ prop: 'preSettlementNos', label: '预结算单号', type: 'input' },
|
||||
{ prop: 'projectName', label: '项目', type: 'input' },
|
||||
{ prop: 'deptName', label: '所属组织', type: 'input' },
|
||||
{ prop: 'contractNo', label: '合同编号', type: 'input' },
|
||||
{ prop: 'payerName', label: '付款方', type: 'input' },
|
||||
{ prop: 'payeeName', label: '收款方', type: 'input' },
|
||||
{
|
||||
prop: 'matchStatus',
|
||||
label: '匹配状态',
|
||||
type: 'select',
|
||||
options: reconciliationMatchStatusOptions,
|
||||
},
|
||||
{
|
||||
prop: 'reconciliationStatus',
|
||||
label: '对账状态',
|
||||
type: 'select',
|
||||
options: reconciliationStatusOptions,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,77 @@
|
||||
export const transportReconciliationTableColumns = [
|
||||
{ prop: 'reconciliationNo', label: '对账单号', minWidth: 150, link: true },
|
||||
{ prop: 'payerName', label: '付款方', minWidth: 150 },
|
||||
{ prop: 'payeeName', label: '收款方', minWidth: 150 },
|
||||
{ prop: 'projectName', label: '项目名称', minWidth: 120 },
|
||||
{ prop: 'deptName', label: '所属组织', minWidth: 120 },
|
||||
{ prop: 'contractNo', label: '合同编号', minWidth: 130 },
|
||||
{ prop: 'contractName', label: '合同名称', minWidth: 150 },
|
||||
{ prop: 'settlementAmount', label: '结算金额', minWidth: 120, money: true },
|
||||
{ prop: 'reconciliationModeName', label: '对账模式', minWidth: 110 },
|
||||
{ prop: 'externalBillCount', label: '账单总数(条)', minWidth: 110 },
|
||||
{ prop: 'matchedCount', label: '匹配数(条)', minWidth: 100 },
|
||||
{ prop: 'reconciliationStatusName', label: '对账状态', minWidth: 100 },
|
||||
{ prop: 'createUserName', label: '创建人', minWidth: 100 },
|
||||
{ prop: 'createTime', label: '创建时间', minWidth: 160 },
|
||||
];
|
||||
|
||||
export const internalColumns = [
|
||||
{ prop: 'documentNo', label: '单据号', minWidth: 150 },
|
||||
{ prop: 'waybillNo', label: '运单号', minWidth: 120 },
|
||||
{ prop: 'matchedExternalLineNo', label: '匹配外部账单行号', minWidth: 140 },
|
||||
{ prop: 'vehicleNo', label: '车号', minWidth: 100 },
|
||||
{ prop: 'departureAddress', label: '发货地址', minWidth: 180 },
|
||||
{ prop: 'arrivalAddress', label: '到货地址', minWidth: 180 },
|
||||
{ prop: 'actualDepartureTime', label: '实际发货时间', minWidth: 150 },
|
||||
{ prop: 'actualCompletionTime', label: '实际完成时间', minWidth: 150 },
|
||||
{ prop: 'transportType', label: '运输类型', minWidth: 120 },
|
||||
{ prop: 'cargoName', label: '货物名称', minWidth: 120 },
|
||||
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
|
||||
{ prop: 'specification', label: '规格', minWidth: 100 },
|
||||
{ prop: 'model', label: '型号', minWidth: 100 },
|
||||
{ prop: 'transportQuantity', label: '运输总量', minWidth: 100, number: true },
|
||||
{ prop: 'mileage', label: '里程(KM)', minWidth: 100, number: true },
|
||||
{ prop: 'batchNo', label: '批次号', minWidth: 100 },
|
||||
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
||||
{ prop: 'freightAmount', label: '运输费', minWidth: 110, money: true },
|
||||
{ prop: 'settlementAmount', label: '结算金额', minWidth: 120, money: true },
|
||||
{ prop: 'matchResult', label: '匹配结果', minWidth: 100 },
|
||||
{ prop: 'updateResult', label: '账单更新结果', minWidth: 130 },
|
||||
];
|
||||
|
||||
export const externalVehicleColumns = [
|
||||
{ prop: 'externalLineNo', label: '序号', minWidth: 70 },
|
||||
{ prop: 'vehicleNo', label: '车牌号', minWidth: 110 },
|
||||
{ prop: 'departureAddress', label: '发货地址', minWidth: 180 },
|
||||
{ prop: 'arrivalAddress', label: '到货地址', minWidth: 180 },
|
||||
{ prop: 'actualDepartureTime', label: '实际发货时间', minWidth: 150 },
|
||||
{ prop: 'actualCompletionTime', label: '实际完成时间', minWidth: 150 },
|
||||
{ prop: 'transportType', label: '运输类型', minWidth: 120 },
|
||||
{ prop: 'cargoName', label: '货物名称', minWidth: 120 },
|
||||
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
|
||||
{ prop: 'transportQuantity', label: '运输总量', minWidth: 100, number: true },
|
||||
{ prop: 'mileage', label: '里程(KM)', minWidth: 100, number: true },
|
||||
{ prop: 'batchNo', label: '批次号', minWidth: 100 },
|
||||
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
||||
{ prop: 'freightAmount', label: '运费', minWidth: 110, money: true },
|
||||
{ prop: 'settlementAmount', label: '结算费用合计', minWidth: 130, money: true },
|
||||
{ prop: 'matchStatus', label: '匹配状态', minWidth: 110 },
|
||||
];
|
||||
|
||||
export const externalCargoColumns = [
|
||||
{ prop: 'externalLineNo', label: '序号', minWidth: 70 },
|
||||
{ prop: 'vehicleNo', label: '车牌号', minWidth: 110 },
|
||||
{ prop: 'departureAddress', label: '发货地址', minWidth: 180 },
|
||||
{ prop: 'arrivalAddress', label: '到货地址', minWidth: 180 },
|
||||
{ prop: 'actualDepartureTime', label: '实际发货时间', minWidth: 150 },
|
||||
{ prop: 'actualCompletionTime', label: '实际完成时间', minWidth: 150 },
|
||||
{ prop: 'cargoName', label: '货物名称', minWidth: 120 },
|
||||
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
|
||||
{ prop: 'specification', label: '规格', minWidth: 100 },
|
||||
{ prop: 'model', label: '型号', minWidth: 100 },
|
||||
{ prop: 'transportQuantity', label: '运输量', minWidth: 100, number: true },
|
||||
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
||||
{ prop: 'mileage', label: '里程(KM)', minWidth: 100, number: true },
|
||||
{ prop: 'freightAmount', label: '运输费', minWidth: 110, money: true },
|
||||
{ prop: 'settlementAmount', label: '结算金额', minWidth: 120, money: true },
|
||||
];
|
||||
@@ -23,11 +23,7 @@ export const insuranceTypeDic = [
|
||||
{ label: '船舶险', value: '船舶险' },
|
||||
];
|
||||
|
||||
export const ocrTemplateDic = [
|
||||
{ label: '紫金-机动车交强险', value: '紫金-机动车交强险' },
|
||||
{ label: '机动车商业险', value: '机动车商业险' },
|
||||
{ label: '船舶保险', value: '船舶保险' },
|
||||
];
|
||||
export const ocrTemplateDic = [];
|
||||
|
||||
export const option = {
|
||||
height: 'auto',
|
||||
@@ -67,9 +63,14 @@ export const option = {
|
||||
prop: 'ocrTemplate',
|
||||
type: 'select',
|
||||
dicData: ocrTemplateDic,
|
||||
value: '紫金-机动车交强险',
|
||||
value: '',
|
||||
hide: true,
|
||||
display: true,
|
||||
addDisplay: true,
|
||||
editDisplay: true,
|
||||
viewDisplay: true,
|
||||
span: 12,
|
||||
rules: [{ required: true, message: '请选择OCR识别模板', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '上传文件',
|
||||
|
||||
@@ -2,6 +2,102 @@ import Layout from '@/page/index/index.vue';
|
||||
import Store from '@/store/';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/business/contract-manage/form',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '新增合同管理',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/contract-manage.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/business/project-apply/form',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '新增项目申请',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/project-apply.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/business/waybill-manage/form',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '新增编辑运单管理',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/waybill-manage.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/business/loading-manage/form',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '新增编辑配载管理',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/loading-manage.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/business/transport-plan/form',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '新增编辑运输计划',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/transport-plan.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/business/shipping-template/form',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '新增编辑运单模板',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/shipping-template.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/vehicle/customer-archive/form',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '新增客商档案',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/vehicle/customer-archive.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/business/contract-manage/change',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '合同变更',
|
||||
meta: { keepAlive: false },
|
||||
component: () => import('@/views/business/contract-manage-change.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/wel',
|
||||
component: () =>
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<basic-container class="insurance-ocr-template-page">
|
||||
<div class="insurance-ocr-template-page__toolbar">
|
||||
<el-button
|
||||
v-if="hasPermission('insurance_ocr_template_add')"
|
||||
type="primary"
|
||||
icon="el-icon-plus"
|
||||
@click="openDialog()"
|
||||
>
|
||||
新增
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('insurance_ocr_template_delete')"
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-delete"
|
||||
:disabled="!selectionList.length"
|
||||
@click="handleRemove(selectionIds)"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
<div class="insurance-ocr-template-page__search">
|
||||
<el-input
|
||||
v-model="query.name"
|
||||
clearable
|
||||
maxlength="100"
|
||||
placeholder="模板名称"
|
||||
@keyup.enter="search"
|
||||
@clear="search"
|
||||
/>
|
||||
<el-button type="primary" icon="el-icon-search" @click="search">查询</el-button>
|
||||
<el-button icon="el-icon-refresh" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="data"
|
||||
border
|
||||
@selection-change="selectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="48" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column prop="name" label="模板名称" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="已配置字段" min-width="150">
|
||||
<template #default="{ row }">{{ getMappingCount(row.mappingConfig) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updateUserName" label="更新人" min-width="120" />
|
||||
<el-table-column prop="updateTime" label="更新时间" min-width="180" />
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="hasPermission('insurance_ocr_template_view')"
|
||||
type="primary"
|
||||
@click="openDialog(row, true)"
|
||||
>
|
||||
查看
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('insurance_ocr_template_edit')"
|
||||
type="primary"
|
||||
@click="openDialog(row)"
|
||||
>
|
||||
编辑
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('insurance_ocr_template_delete')"
|
||||
type="danger"
|
||||
@click="handleRemove(row.id)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<empty-pagination
|
||||
:page="page"
|
||||
@size-change="sizeChange"
|
||||
@current-change="currentChange"
|
||||
@load="onLoad"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="860px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="templateForm" :model="form" :rules="rules" label-width="100px" :disabled="readonly">
|
||||
<el-form-item label="模板名称" prop="name">
|
||||
<el-input v-model="form.name" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="字段映射" required>
|
||||
<el-table :data="mappingRows" border class="insurance-ocr-template-page__mapping-table">
|
||||
<el-table-column prop="key" label="键名" min-width="240" />
|
||||
<el-table-column label="自定义映射值" min-width="500">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.value"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
placeholder="请输入保险单据中的对应字段名称"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button v-if="!readonly" type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getDetail, getList, remove, submit } from '@/api/base/insurance-ocr-template';
|
||||
|
||||
const insuranceFieldKeys = [
|
||||
'保险类型',
|
||||
'保单号',
|
||||
'开始日期',
|
||||
'结束日期',
|
||||
'保额',
|
||||
'保费',
|
||||
'发票号',
|
||||
'开票日期',
|
||||
'备注',
|
||||
];
|
||||
|
||||
const createMappingRows = mappingConfig => {
|
||||
let valueMap = {};
|
||||
try {
|
||||
const mappings = JSON.parse(mappingConfig || '[]');
|
||||
if (Array.isArray(mappings)) {
|
||||
valueMap = mappings.reduce((result, item) => {
|
||||
if (insuranceFieldKeys.includes(item?.key)) result[item.key] = item.value || '';
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
} catch (error) {
|
||||
valueMap = {};
|
||||
}
|
||||
return insuranceFieldKeys.map(key => ({ key, value: valueMap[key] || '' }));
|
||||
};
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
submitLoading: false,
|
||||
dialogVisible: false,
|
||||
readonly: false,
|
||||
data: [],
|
||||
selectionList: [],
|
||||
query: { name: '' },
|
||||
form: { id: '', name: '' },
|
||||
mappingRows: createMappingRows(),
|
||||
page: {
|
||||
pageSize: 10,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
},
|
||||
rules: {
|
||||
name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
return String(this.userInfo.authority || '').includes('admin');
|
||||
},
|
||||
selectionIds() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
dialogTitle() {
|
||||
if (this.readonly) return '查看保险OCR识别模板';
|
||||
return this.form.id ? '编辑保险OCR识别模板' : '新增保险OCR识别模板';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.permission?.[code] === true;
|
||||
},
|
||||
onLoad() {
|
||||
this.loading = true;
|
||||
getList(this.page.currentPage, this.page.pageSize, this.query)
|
||||
.then(res => {
|
||||
const pageData = res.data.data || {};
|
||||
this.data = pageData.records || [];
|
||||
this.page.total = pageData.total || 0;
|
||||
this.selectionList = [];
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
search() {
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad();
|
||||
},
|
||||
resetSearch() {
|
||||
this.query.name = '';
|
||||
this.search();
|
||||
},
|
||||
currentChange(currentPage) {
|
||||
this.page.currentPage = currentPage;
|
||||
this.onLoad();
|
||||
},
|
||||
sizeChange(pageSize) {
|
||||
this.page.pageSize = pageSize;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad();
|
||||
},
|
||||
selectionChange(selectionList) {
|
||||
this.selectionList = selectionList;
|
||||
},
|
||||
openDialog(row, readonly = false) {
|
||||
this.readonly = readonly;
|
||||
if (!row?.id) {
|
||||
this.dialogVisible = true;
|
||||
return;
|
||||
}
|
||||
getDetail(row.id).then(res => {
|
||||
const detail = res.data.data || {};
|
||||
this.form = { id: detail.id, name: detail.name || '' };
|
||||
this.mappingRows = createMappingRows(detail.mappingConfig);
|
||||
this.dialogVisible = true;
|
||||
});
|
||||
},
|
||||
resetForm() {
|
||||
this.form = { id: '', name: '' };
|
||||
this.mappingRows = createMappingRows();
|
||||
this.readonly = false;
|
||||
this.submitLoading = false;
|
||||
this.$refs.templateForm?.clearValidate();
|
||||
},
|
||||
handleSubmit() {
|
||||
const hasMappingValue = this.mappingRows.some(item => String(item.value || '').trim());
|
||||
if (!hasMappingValue) {
|
||||
this.$message.warning('请至少填写一个字段映射值');
|
||||
return;
|
||||
}
|
||||
this.$refs.templateForm.validate(valid => {
|
||||
if (!valid) return;
|
||||
this.submitLoading = true;
|
||||
submit({
|
||||
id: this.form.id || undefined,
|
||||
name: String(this.form.name || '').trim(),
|
||||
mappingConfig: JSON.stringify(
|
||||
this.mappingRows.map(item => ({ key: item.key, value: String(item.value || '').trim() }))
|
||||
),
|
||||
})
|
||||
.then(() => {
|
||||
this.$message.success('操作成功');
|
||||
this.dialogVisible = false;
|
||||
this.onLoad();
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitLoading = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
handleRemove(ids) {
|
||||
this.$confirm('确定删除选中的保险OCR识别模板吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
remove(ids).then(() => {
|
||||
this.$message.success('操作成功');
|
||||
this.onLoad();
|
||||
});
|
||||
});
|
||||
},
|
||||
getMappingCount(mappingConfig) {
|
||||
return createMappingRows(mappingConfig).filter(item => String(item.value || '').trim()).length;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.insurance-ocr-template-page {
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
width: 380px;
|
||||
}
|
||||
|
||||
&__mapping-table {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="readonly ? '计费方案详情' : index < 0 ? '添加计费方案' : '编辑计费方案'" width="92%" append-to-body destroy-on-close>
|
||||
<el-form ref="formRef" :model="draft" :rules="formRules" label-position="right" label-width="auto">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="方案名称" prop="planName"><el-input v-model="draft.planName" maxlength="100" :disabled="readonly" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="默认方案"><el-checkbox v-model="draft.defaultPlan" :disabled="readonly">默认方案</el-checkbox></el-form-item></el-col>
|
||||
<el-col :span="24"><el-form-item label="备注"><el-input v-model="draft.remark" maxlength="100" show-word-limit :disabled="readonly" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<div class="rule-head"><el-link v-if="!readonly" type="primary" @click="addRule">+添加规则</el-link></div>
|
||||
<el-table :data="draft.rules" border>
|
||||
<el-table-column type="index" label="序号" width="65" />
|
||||
<el-table-column label="费用类型" width="180"><template #default="{ row }"><el-select v-model="row.feeType" clearable filterable :disabled="readonly" :loading="feeCategoryLoading" @change="value => handleFeeTypeChange(row, value)"><el-option v-for="item in feeCategories" :key="item.dictKey" :label="item.dictValue" :value="item.dictKey" /></el-select></template></el-table-column>
|
||||
<el-table-column label="费用项" width="210"><template #default="{ row }"><el-select v-model="row.feeItem" clearable filterable :disabled="readonly || !row.feeType" :loading="feeItemLoadingMap[feeTypeKey(row)]"><el-option v-for="item in feeItemOptions(row)" :key="item.id || item.name || item.englishName" :label="item.name || item.englishName" :value="item.name || item.englishName" /></el-select></template></el-table-column>
|
||||
<el-table-column label="计费要素" width="170"><template #default="{ row }"><el-select v-model="row.billingElement" :disabled="readonly" placeholder="请选择" @change="handleElementChange(row)"><el-option v-for="item in billingElements" :key="item" :label="item" :value="item" /></el-select></template></el-table-column>
|
||||
<el-table-column label="计费类型" width="180"><template #default="{ row }"><el-select v-model="row.billingType" :disabled="readonly || !row.billingElement" placeholder="请选择" @change="handleTypeChange(row)"><el-option v-for="item in billingTypes(row)" :key="item" :label="item" :value="item" /></el-select></template></el-table-column>
|
||||
<el-table-column label="计费单位" width="150"><template #default="{ row }"><el-select v-model="row.billingUnit" clearable filterable :disabled="readonly" :loading="unitLoading"><el-option v-for="item in unitOptions" :key="item.id || item.dictKey || item.dictValue" :label="item.dictValue" :value="item.dictValue" /></el-select></template></el-table-column>
|
||||
<el-table-column label="单价(元)" width="180"><template #default="{ row }"><div v-if="usesRangeUnitPrice(row)" class="limit-list"><el-input v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex" v-model="item.unitPrice" :disabled="readonly" placeholder="请输入单价" @input="value => rangeUnitPriceInput(row, rangeIndex, value)" /></div><el-input v-else v-model="row.unitPrice" :disabled="readonly" @input="value => decimalInput(row, 'unitPrice', value)" /></template></el-table-column>
|
||||
<el-table-column label="计费要素下限" width="250"><template #default="{ row }"><div class="limit-list"><el-input v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex" v-model="item.lowerLimit" :disabled="!canEditLimit(row)" :placeholder="canEditLimit(row) ? '请输入下限' : '无需配置'" @input="value => limitInput(row, rangeIndex, 'lowerLimit', value)" /></div></template></el-table-column>
|
||||
<el-table-column label="计费要素上限" width="270"><template #default="{ row }"><div class="limit-list"><div v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex" class="limit-row"><el-input v-model="item.upperLimit" :disabled="!canEditLimit(row)" :placeholder="canEditLimit(row) ? '请输入上限' : '无需配置'" @input="value => limitInput(row, rangeIndex, 'upperLimit', value)" /><el-link v-if="canEditLimit(row)" type="primary" @click="addRange(row, rangeIndex)">添加</el-link><el-link v-if="canEditLimit(row) && getRanges(row).length > 1" type="danger" @click="removeRange(row, rangeIndex)">删除</el-link></div></div></template></el-table-column>
|
||||
<el-table-column label="保底计费重量" width="240"><template #default="{ row }"><el-input v-model="row.minimumBillingWeight" :disabled="!canEditMinimum(row)" :placeholder="canEditMinimum(row) ? '实际计量值小于保底数值时按保底计费' : '仅按重量、按吨·公里可填写'" @input="value => decimalInput(row, 'minimumBillingWeight', value)" /></template></el-table-column>
|
||||
<el-table-column label="备注" width="180"><template #default="{ row }"><el-input v-model="row.remark" maxlength="100" show-word-limit :disabled="readonly" /></template></el-table-column>
|
||||
<el-table-column label="操作" width="250" fixed="right"><template #default="{ row, $index }"><el-link type="primary" @click="openMatch(row, $index)">{{ readonly ? '查看匹配条件' : '设置匹配条件' }}</el-link><el-link v-if="!readonly" type="primary" @click="copyRule(row)">复制</el-link><el-link v-if="!readonly" type="danger" @click="removeRule($index)">删除</el-link></template></el-table-column>
|
||||
</el-table>
|
||||
<template #footer><el-button v-if="readonly" type="primary" @click="visible = false">关闭</el-button><template v-else><el-button @click="visible = false">取消</el-button><el-button type="primary" @click="save">提交</el-button></template></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="matchVisible" title="设置匹配条件" append-to-body width="720px">
|
||||
<el-form :model="matchForm" label-position="right" label-width="90px">
|
||||
<el-row :gutter="48">
|
||||
<el-col :span="12"><el-form-item label="起运地"><el-cascader v-model="matchForm.originPath" :options="regionOptions" :props="regionProps" :placeholder="matchForm.origin || '请选择起运地'" :loading="regionLoading" :disabled="readonly" clearable filterable @visible-change="v => v && ensureRegions()" @change="v => matchRegionChange('origin', v)" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="目的地"><el-cascader v-model="matchForm.destinationPath" :options="regionOptions" :props="regionProps" :placeholder="matchForm.destination || '请选择目的地'" :loading="regionLoading" :disabled="readonly" clearable filterable @visible-change="v => v && ensureRegions()" @change="v => matchRegionChange('destination', v)" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="运输方式"><el-select v-model="matchForm.transportMode" :disabled="readonly" clearable><el-option label="公路" value="公路" /><el-option label="铁路" value="铁路" /><el-option label="水路" value="水路" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="货物类型"><el-cascader v-model="matchForm.cargoTypePath" :options="cargoOptions" :props="cargoProps" :placeholder="matchForm.cargoType || '请选择货物类型'" :loading="cargoLoading" :disabled="readonly" clearable filterable :filter-method="filterCargo" @visible-change="v => v && ensureCargo()" @change="matchCargoChange" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer><el-button v-if="readonly" type="primary" @click="matchVisible = false">关闭</el-button><template v-else><el-button @click="matchVisible = false">取消</el-button><el-button type="primary" @click="saveMatch">保存</el-button></template></template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { getList as getFeeItemList } from '@/api/base/fee-item';
|
||||
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
|
||||
const clone = value => JSON.parse(JSON.stringify(value));
|
||||
const defaultRule = () => ({ feeType: '', feeItem: '', billingElement: '', billingType: '', billingUnit: '', unitPrice: '', lowerLimit: '', upperLimit: '', limitRanges: [{ lowerLimit: '', upperLimit: '', unitPrice: '' }], minimumBillingWeight: '', remark: '', matchCondition: { origin: '', originCode: '', originPath: [], destination: '', destinationCode: '', destinationPath: [], transportMode: '', cargoType: '', cargoTypeCode: '', cargoTypePath: [] } });
|
||||
|
||||
export default {
|
||||
props: { modelValue: Boolean, value: { type: Object, default: () => ({}) }, index: { type: Number, default: -1 }, readonly: Boolean },
|
||||
emits: ['update:modelValue', 'save'],
|
||||
data() { return { draft: this.normalizePlan(this.value), feeCategories: [], feeCategoryLoading: false, feeItems: {}, feeItemLoadingMap: {}, unitOptions: [], unitLoading: false, billingElements: ['按重量', '按体积', '按车辆', '按里程', '按吨·公里', '固定金额(整单一口价)', '按数量'], typeMap: { 按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], 按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], 按车辆: ['固定单价'], 按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], '按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], '固定金额(整单一口价)': ['固定一口价'], 按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'] }, matchVisible: false, matchIndex: -1, matchForm: defaultRule().matchCondition, regionOptions: [], regionLoading: false, regionRequest: null, cargoOptions: [], cargoFlatOptions: [], cargoLoading: false, cargoRequest: null, formRules: { planName: [{ required: true, message: '请输入方案名称', trigger: 'blur' }] } }; },
|
||||
computed: { visible: { get() { return this.modelValue; }, set(value) { this.$emit('update:modelValue', value); } }, regionProps() { return { label: 'title', value: 'id', children: 'children', leaf: 'leaf', emitPath: true }; }, cargoProps() { return { label: 'cargoName', value: 'id', children: 'children', disabled: (data, node) => node.level === 1, leaf: 'leaf', checkStrictly: true, emitPath: true }; } },
|
||||
mounted() { this.loadDictionaries(); },
|
||||
watch: { value: { deep: true, handler(value) { if (this.modelValue) { this.draft = this.normalizePlan(value); this.loadRuleItems(); } } } },
|
||||
methods: {
|
||||
normalizePlan(value = {}) { const plan = { planName: '', defaultPlan: false, remark: '', ...clone(value || {}) }; plan.rules = (value?.rules?.length ? value.rules : [defaultRule()]).map(rule => this.normalizeRule(rule)); return plan; },
|
||||
normalizeRule(rule = {}) { const next = { ...defaultRule(), ...clone(rule) }; next.limitRanges = this.normalizeRanges(next); this.syncLegacyLimit(next); next.matchCondition = { ...defaultRule().matchCondition, ...(rule.matchCondition || {}) }; return next; },
|
||||
normalizeRanges(row) { const ranges = Array.isArray(row.limitRanges) ? row.limitRanges.map(item => ({ lowerLimit: item?.lowerLimit ?? '', upperLimit: item?.upperLimit ?? '', unitPrice: item?.unitPrice === undefined || item?.unitPrice === '' ? row.unitPrice ?? '' : item.unitPrice })).filter(item => item.lowerLimit !== '' || item.upperLimit !== '' || item.unitPrice !== '') : []; if (!ranges.length && (row.lowerLimit !== '' || row.upperLimit !== '' || row.unitPrice !== '')) ranges.push({ lowerLimit: row.lowerLimit ?? '', upperLimit: row.upperLimit ?? '', unitPrice: row.unitPrice ?? '' }); return ranges.length ? ranges : [{ lowerLimit: '', upperLimit: '', unitPrice: '' }]; },
|
||||
syncLegacyLimit(row) { const first = row.limitRanges?.[0] || {}; row.lowerLimit = first.lowerLimit || ''; row.upperLimit = first.upperLimit || ''; if (this.usesRangeUnitPrice(row)) row.unitPrice = first.unitPrice || ''; },
|
||||
loadDictionaries() { this.feeCategoryLoading = true; getDictionary({ code: 'fee_category' }).then(res => { this.feeCategories = res.data?.data || []; this.loadRuleItems(); }).finally(() => { this.feeCategoryLoading = false; }); this.unitLoading = true; getDictionary({ code: 'unit_fee' }).then(res => { this.unitOptions = res.data?.data || []; }).finally(() => { this.unitLoading = false; }); },
|
||||
feeTypeKey(row) { const option = this.feeCategories.find(item => String(item.dictKey) === String(row.feeType) || String(item.dictValue) === String(row.feeType)); return option?.dictKey || row.feeType || ''; },
|
||||
feeItemOptions(row) { return this.feeItems[this.feeTypeKey(row)] || []; },
|
||||
loadRuleItems() { (this.draft.rules || []).forEach(row => this.loadFeeItems(row.feeType)); },
|
||||
loadFeeItems(value) { const key = this.feeTypeKey({ feeType: value }); if (!key || this.feeItems[key]) return; this.feeItemLoadingMap[key] = true; getFeeItemList(1, 9999, { feeCategory: key }).then(res => { const data = res?.data?.data || res?.data || {}; this.feeItems[key] = Array.isArray(data) ? data : (data.records || []); }).finally(() => { this.feeItemLoadingMap[key] = false; }); },
|
||||
handleFeeTypeChange(row, value) { row.feeType = this.feeTypeKey({ feeType: value }); row.feeItem = ''; this.loadFeeItems(row.feeType); },
|
||||
billingTypes(row) { return this.typeMap[row.billingElement] || []; },
|
||||
handleElementChange(row) { if (!this.billingTypes(row).includes(row.billingType)) row.billingType = ''; if (!this.canEditMinimum(row)) row.minimumBillingWeight = ''; this.handleTypeChange(row); },
|
||||
handleTypeChange(row) { if (!this.canEditLimit(row)) { row.limitRanges = [{ lowerLimit: '', upperLimit: '', unitPrice: '' }]; row.lowerLimit = ''; row.upperLimit = ''; } else { row.limitRanges = this.normalizeRanges(row); this.syncLegacyLimit(row); } },
|
||||
canEditLimit(row) { return !this.readonly && Boolean(row.billingElement) && Boolean(row.billingType) && (row.billingType.includes('区间') || row.billingType.includes('阶梯')); },
|
||||
usesRangeUnitPrice(row) { return ['区间单价', '阶梯单价'].includes(row.billingType); },
|
||||
canEditMinimum(row) { return !this.readonly && ['按重量', '按吨·公里'].includes(row.billingElement); },
|
||||
getRanges(row) { return Array.isArray(row.limitRanges) && row.limitRanges.length ? row.limitRanges : [{ lowerLimit: '', upperLimit: '', unitPrice: '' }]; },
|
||||
addRange(row, index) { row.limitRanges = this.getRanges(row); row.limitRanges.splice(index + 1, 0, { lowerLimit: '', upperLimit: '', unitPrice: '' }); },
|
||||
removeRange(row, index) { row.limitRanges = this.getRanges(row); row.limitRanges.splice(index, 1); if (!row.limitRanges.length) row.limitRanges.push({ lowerLimit: '', upperLimit: '', unitPrice: '' }); this.syncLegacyLimit(row); },
|
||||
limitInput(row, index, prop, value) { row.limitRanges = this.getRanges(row); const text = String(value || '').replace(/[^\d.]/g, ''); const parts = text.split('.'); row.limitRanges[index][prop] = parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0]; this.syncLegacyLimit(row); },
|
||||
decimalInput(row, prop, value) { const text = String(value || '').replace(/[^\d.]/g, ''); const parts = text.split('.'); row[prop] = parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0]; },
|
||||
rangeUnitPriceInput(row, index, value) { row.limitRanges = this.getRanges(row); const target = { value }; this.decimalInput(target, 'value', value); row.limitRanges[index].unitPrice = target.value; if (index === 0) row.unitPrice = target.value; },
|
||||
addRule() { this.draft.rules.push(defaultRule()); },
|
||||
copyRule(row) { const next = clone(row); this.draft.rules.push(this.normalizeRule(next)); this.loadFeeItems(next.feeType); },
|
||||
removeRule(index) { this.draft.rules.splice(index, 1); if (!this.draft.rules.length) this.addRule(); },
|
||||
validateRules() { const groups = {}; const feeItemSet = new Set(); const required = [['feeType', '费用类型'], ['feeItem', '费用项'], ['billingElement', '计费要素'], ['billingType', '计费类型'], ['billingUnit', '计费单位']]; for (const [i, row] of this.draft.rules.entries()) { const empty = required.find(([key]) => row[key] === undefined || row[key] === null || String(row[key]).trim() === ''); if (empty) { this.$message.warning(`第${i + 1}行${empty[1]}不能为空`); return false; } const feeItem = String(row.feeItem).trim(); if (feeItemSet.has(feeItem)) { this.$message.warning(`费用项“${feeItem}”不能重复`); return false; } feeItemSet.add(feeItem); if (!this.usesRangeUnitPrice(row) && (row.unitPrice === undefined || row.unitPrice === null || String(row.unitPrice).trim() === '')) { this.$message.warning(`第${i + 1}行单价不能为空`); return false; } if (!this.billingTypes(row).includes(row.billingType)) { this.$message.warning('请选择计费要素对应的计费类型'); return false; } if (!this.canEditLimit(row)) continue; const key = row.billingElement; groups[key] = groups[key] || []; for (const range of this.getRanges(row)) { const lower = Number(range.lowerLimit); const upper = Number(range.upperLimit); if (range.lowerLimit === '' || range.upperLimit === '' || Number.isNaN(lower) || Number.isNaN(upper)) { this.$message.warning('请完整填写计费要素上下限'); return false; } if (this.usesRangeUnitPrice(row) && (range.unitPrice === undefined || range.unitPrice === null || String(range.unitPrice).trim() === '')) { this.$message.warning('请为每组计费要素区间填写单价'); return false; } if (lower > upper) { this.$message.warning('计费要素下限不能大于上限'); return false; } groups[key].push({ lower, upper }); } } return this.validateRangeGroups(groups); },
|
||||
validateRangeGroups(groups) { const precision = 0.000001; for (const [element, ranges] of Object.entries(groups)) { const sorted = [...ranges].sort((a, b) => a.lower - b.lower || a.upper - b.upper); for (let i = 1; i < sorted.length; i += 1) { if (sorted[i].lower < sorted[i - 1].upper - precision) { this.$message.warning(`${element}的计费要素区间不能重叠`); return false; } if (sorted[i].lower > sorted[i - 1].upper + precision) { this.$message.warning(`${element}的计费要素区间必须连续,不能存在间隙`); return false; } } } return true; },
|
||||
save() { this.$refs.formRef.validate(valid => { if (!valid || !this.validateRules()) return; const plan = clone(this.draft); plan.rules = plan.rules.map(rule => { const ranges = this.canEditLimit(rule) ? this.getRanges(rule) : []; return { ...rule, unitPrice: this.usesRangeUnitPrice(rule) ? ranges[0]?.unitPrice || '' : rule.unitPrice, limitRanges: ranges, lowerLimit: ranges[0]?.lowerLimit || '', upperLimit: ranges[0]?.upperLimit || '', minimumBillingWeight: this.canEditMinimum(rule) ? rule.minimumBillingWeight : '' }; }); this.$emit('save', plan, this.index); this.visible = false; }); },
|
||||
openMatch(row, index) { this.matchIndex = index; this.matchForm = { ...defaultRule().matchCondition, ...(row.matchCondition || {}) }; this.matchVisible = true; this.ensureRegions(); this.ensureCargo().then(() => { const path = this.resolveCargoPath(this.matchForm); if (path.length) this.matchCargoChange(path); }); },
|
||||
ensureRegions() { if (this.regionOptions.length) return Promise.resolve(this.regionOptions); if (this.regionRequest) return this.regionRequest; this.regionLoading = true; this.regionRequest = getRegionLazyTree().then(res => { const raw = res.data?.data || []; this.regionOptions = this.normalizeRegionTree(raw); return this.regionOptions; }).finally(() => { this.regionLoading = false; this.regionRequest = null; }); return this.regionRequest; },
|
||||
normalizeRegionTree(list) { const map = new Map(); const flat = []; const walk = items => (items || []).forEach(item => { flat.push({ ...item, children: undefined }); if (item.children?.length) walk(item.children); }); walk(list); flat.forEach(item => map.set(String(item.id ?? item.code ?? item.value), { ...item, id: String(item.id ?? item.code ?? item.value), parentId: String(item.parentId ?? item.parentCode ?? ''), children: [] })); const roots = []; map.forEach(node => { const parent = map.get(node.parentId); if (parent && parent !== node) parent.children.push(node); else roots.push(node); }); const china = roots.find(item => ['中国', '中华人民共和国'].includes(item.title || item.name)); const source = china?.children?.length ? china.children : roots; const normalize = (node, level) => ({ ...node, title: node.title || node.name || '', leaf: level === 2, children: level === 1 ? (node.children || []).map(child => normalize(child, 2)) : undefined }); return source.map(node => normalize(node, 1)); },
|
||||
regionLabels(path) { let options = this.regionOptions; const labels = []; (path || []).forEach(value => { const option = options.find(item => String(item.id) === String(value)); if (option) { labels.push(option.title || option.name || ''); options = option.children || []; } }); return labels; },
|
||||
matchRegionChange(prop, value) { const path = Array.isArray(value) && value.length >= 2 ? value.slice(0, 2) : []; this.matchForm[`${prop}Path`] = path; const labels = this.regionLabels(path); this.matchForm[prop] = labels.join(''); this.matchForm[`${prop}Code`] = path.length ? String(path[path.length - 1]) : ''; },
|
||||
ensureCargo() { if (this.cargoOptions.length) return Promise.resolve(this.cargoOptions); if (this.cargoRequest) return this.cargoRequest; this.cargoLoading = true; this.cargoRequest = getCargoTypeList(1, 9999).then(res => { const data = res?.data?.data || res?.data || {}; const records = Array.isArray(data) ? data : (data.records || []); this.cargoOptions = this.buildCargoTree(records); this.cargoFlatOptions = this.flatten(this.cargoOptions); return this.cargoOptions; }).finally(() => { this.cargoLoading = false; this.cargoRequest = null; }); return this.cargoRequest; },
|
||||
buildCargoTree(records) { const flat = []; const walk = items => (items || []).forEach(item => { flat.push(item); if (item.children?.length) walk(item.children); }); walk(records); const map = new Map(); flat.forEach(item => { const id = String(item.id ?? item.cargoCode ?? item.code); map.set(id, { ...item, id, cargoName: item.cargoName || item.name || item.typeName || '', parentId: String(item.parentId ?? item.parentCode ?? ''), children: [] }); }); const roots = []; map.forEach(node => { const parent = map.get(node.parentId); if (parent && parent !== node && Number(node.typeLevel) !== 1) parent.children.push(node); else roots.push(node); }); const normalize = (node, level, parent = []) => { const path = [...parent, node.id]; const children = level === 1 ? node.children.map(item => normalize(item, 2, path)) : []; return { ...node, path, leaf: level === 2, children: children.length ? children : undefined }; }; return roots.map(node => normalize(node, 1)); },
|
||||
flatten(list) { const result = []; const walk = items => (items || []).forEach(item => { result.push(item); if (item.children?.length) walk(item.children); }); walk(list); return result; },
|
||||
filterCargo(node, keyword) { const text = String(keyword || '').trim(); return !text || String(node.label || node.text || node.cargoName || '').includes(text); },
|
||||
cargoLabels(path) { let options = this.cargoOptions; const labels = []; (path || []).forEach(value => { const option = options.find(item => String(item.id) === String(value)); if (option) { labels.push(option.cargoName || ''); options = option.children || []; } }); return labels; },
|
||||
matchCargoChange(value) { const path = Array.isArray(value) && value.length >= 2 ? value.slice(0, 2).map(item => String(item)) : []; const item = this.cargoFlatOptions.find(option => String(option.id) === String(path[1]) && option.path?.length >= 2); this.matchForm.cargoTypePath = path; this.matchForm.cargoType = item ? this.cargoLabels(path).join('/') : ''; this.matchForm.cargoTypeCode = item ? item.cargoCode || item.code || item.id : ''; },
|
||||
resolveCargoPath(condition) { if (Array.isArray(condition.cargoTypePath) && condition.cargoTypePath.length >= 2) return condition.cargoTypePath; const item = this.cargoFlatOptions.find(option => option.path?.length >= 2 && (String(option.cargoCode || option.code || option.id) === String(condition.cargoTypeCode) || option.cargoName === condition.cargoType)); return item?.path || []; },
|
||||
saveMatch() { const row = this.draft.rules[this.matchIndex]; if (row) { this.matchRegionChange('origin', this.matchForm.originPath); this.matchRegionChange('destination', this.matchForm.destinationPath); this.matchCargoChange(this.matchForm.cargoTypePath); row.matchCondition = clone(this.matchForm); } this.matchVisible = false; },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>.rule-head { margin: 12px 0; }.limit-list { display: flex; flex-direction: column; gap: 8px; }.limit-row { display: flex; align-items: center; gap: 8px; }.limit-row .el-input { flex: 1; }</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -144,7 +144,7 @@ export default {
|
||||
.segment-detail__header-right { display: flex; align-items: center; gap: 12px; }
|
||||
.segment-index { display: inline-flex; width: 36px; height: 36px; align-items: center; justify-content: center; border-radius: 50%; background: #2088ee; color: #fff; font-size: 20px; font-weight: 500; }
|
||||
.segment-execution { padding-top: 12px; }
|
||||
.segment-stats { display: flex; gap: 72px; padding: 4px 0 24px; div { display: flex; flex-direction: column; gap: 8px; } span { color: #8a9bb8; font-size: 14px; } strong { color: #1f2d3d; font-size: 32px; line-height: 1; } small { margin-left: 6px; color: #8a9bb8; font-size: 16px; font-weight: 400; } .dispatched { color: #16a34a; } .remaining { color: #f59e0b; } }
|
||||
.segment-stats { display: flex; gap: 72px; padding: 4px 0 24px; div { display: flex; flex-direction: column; gap: 8px; } span { color: #8a9bb8; font-size: 14px; } strong { color: #1f2d3d; font-size: 32px; line-height: 1; } small { margin-left: 6px; color: #8a9bb8; font-size: 16px; font-weight: 400; } .dispatched { color: #409eff; } .remaining { color: #f56c6c; } }
|
||||
.waybill-table { :deep(th.el-table__cell) { background: #f5f7fa; color: #60738f; } }
|
||||
.transport-plan-detail { margin-top: 20px; h4 { margin: 0 0 12px; padding-left: 12px; border-left: 4px solid #409eff; font-size: 15px; } }
|
||||
.master-goods-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<div><dt>客户</dt><dd>{{ master.customerName || '-' }}</dd></div>
|
||||
<div><dt>合同编号</dt><dd>{{ master.contractNo || '-' }}</dd></div>
|
||||
<div><dt>项目</dt><dd>{{ master.projectName || '-' }}</dd></div>
|
||||
<div><dt>货物名称</dt><dd>{{ goodsNames }}</dd></div>
|
||||
<div><dt>货物类型</dt><dd>{{ goodsTypes }}</dd></div>
|
||||
<div><dt>货物名称</dt><dd class="overview-meta__plain-value">{{ goodsNames }}</dd></div>
|
||||
<div><dt>货物类型</dt><dd class="overview-meta__plain-value">{{ goodsTypes }}</dd></div>
|
||||
<div><dt>运输周期</dt><dd>{{ dateRange }}</dd></div>
|
||||
</dl>
|
||||
<div class="route-overview">
|
||||
@@ -39,7 +39,19 @@
|
||||
<section v-for="(route, index) in routes" :key="route.segmentNo" class="dispatch-section" :data-segment="route.segmentNo">
|
||||
<header class="segment-header">
|
||||
<div>
|
||||
<el-checkbox v-model="route.selected" :label="`${route.segmentNo}:${route.departureName} → ${route.arrivalName}`" />
|
||||
<el-checkbox v-model="route.selected">
|
||||
<span class="segment-checkbox-label">
|
||||
<span>{{ route.segmentNo }}:</span>
|
||||
<el-tooltip
|
||||
:content="`${route.departureName || '-'} → ${route.arrivalName || '-'}`"
|
||||
placement="top"
|
||||
>
|
||||
<span class="segment-checkbox-path"
|
||||
>{{ route.departureName || '-' }} → {{ route.arrivalName || '-' }}</span
|
||||
>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div>总量 {{ formatQuantity(totalQuantity) }},已调度 <em>{{ formatQuantity(dispatchedQuantity(route)) }}</em>,剩余 <em>{{ formatQuantity(remaining(route)) }}</em></div>
|
||||
</header>
|
||||
@@ -49,13 +61,6 @@
|
||||
<el-col :span="8">
|
||||
<el-form-item label="运输方式" required class="transport-type-field"><el-input :model-value="route.transportType" readonly /></el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="isRoad(route)" :span="8">
|
||||
<el-form-item label="单据类型" required>
|
||||
<el-radio-group v-model="route.documentType">
|
||||
<el-radio value="计划单">计划单</el-radio><el-radio value="运单">运单</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
@@ -78,8 +83,8 @@
|
||||
<div class="goods-heading"><h3>货物信息</h3><div><el-link type="primary" @click="addGoods(route)">新增货物</el-link></div></div>
|
||||
<el-table :data="route.goods" border class="goods-table">
|
||||
<el-table-column type="index" label="序号" width="64" />
|
||||
<el-table-column label="货物类型" min-width="180"><template #default="{ row }"><el-cascader v-model="row.cargoTypePath" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" placeholder="请选择货物类型" clearable filterable @change="value => handleCargoTypeChange(route, row, value)" /></template></el-table-column>
|
||||
<el-table-column label="货物名称" min-width="180"><template #default="{ row }"><el-select v-model="row.sourceIndex" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoodsByCargoType(row)" :key="item.sourceIndex" :label="item.cargoName" :value="item.sourceIndex" /></el-select></template></el-table-column>
|
||||
<el-table-column label="货物类型" min-width="180"><template #default="{ row }"><el-cascader v-model="row.cargoTypePath" class="goods-table__cargo-type" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" placeholder="请选择货物类型" clearable filterable @change="value => handleCargoTypeChange(route, row, value)" /></template></el-table-column>
|
||||
<el-table-column label="货物名称" min-width="180"><template #default="{ row }"><el-select v-model="row.sourceIndex" class="goods-table__cargo-name" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoodsByCargoType(row)" :key="item.sourceIndex" :label="item.cargoName" :value="item.sourceIndex" /></el-select></template></el-table-column>
|
||||
<el-table-column prop="remainingQuantity" column-key="remainingQuantity" label="剩余数量" width="150" class-name="goods-table__remaining"><template #default="{ row }">{{ formatQuantity(goodsRemainingQuantity(route, row)) }}</template></el-table-column>
|
||||
<el-table-column prop="dispatchQuantity" column-key="dispatchQuantity" label="本次数量" width="200" class-name="goods-table__dispatch"><template #default="{ row }"><el-input :model-value="row.dispatchQuantity" inputmode="decimal" placeholder="请输入" @input="value => handleDispatchQuantityInput(route, row, value)" /></template></el-table-column>
|
||||
<el-table-column prop="quantityUnit" label="数量单位" width="110" />
|
||||
@@ -95,14 +100,14 @@
|
||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form freight-form">
|
||||
<el-row v-for="(item, index) in route.freightItems" :key="item.sourceIndex" :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="`单价${index + 1}`" required>
|
||||
<el-form-item :label="`单价${index + 1}`">
|
||||
<el-input :model-value="item.unitPrice" inputmode="decimal" placeholder="请输入" @input="value => handleFreightUnitPriceInput(item, value)">
|
||||
<template #append><el-select v-model="item.priceUnit" class="freight-unit-select"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /><el-option label="元/方" value="元/方" /></el-select></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="`数量${index + 1}`" required>
|
||||
<el-form-item :label="`数量${index + 1}`">
|
||||
<el-input :model-value="formatQuantity(item.quantity)" readonly>
|
||||
<template #append><el-select v-model="item.quantityUnit" class="freight-unit-select" @change="value => handleFreightQuantityUnitChange(route, item, value)"><el-option v-for="unit in quantityUnitOptions" :key="unit" :label="unit" :value="unit" /></el-select></template>
|
||||
</el-input>
|
||||
@@ -142,7 +147,7 @@
|
||||
<el-col :span="6"><el-form-item label="箱号" required><el-input v-model="route.containerNo" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="舱位" required><el-input v-model="route.cabinNo" placeholder="请输入" /></el-form-item></el-col>
|
||||
</template>
|
||||
<el-col :span="6"><el-form-item label="里程(km)" required><el-input :model-value="route.mileage" inputmode="numeric" maxlength="10" placeholder="请输入" @input="value => handleMileageInput(route, value)" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="里程(km)"><el-input :model-value="route.mileage" inputmode="numeric" maxlength="10" placeholder="请输入" @input="value => handleMileageInput(route, value)" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="备注"><el-input v-model="route.remark" maxlength="200" show-word-limit placeholder="请输入" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
@@ -433,9 +438,7 @@ export default {
|
||||
validateFreightItems(route) {
|
||||
for (const [index, item] of (route.freightItems || []).entries()) {
|
||||
const fields = [
|
||||
['unitPrice', '单价'],
|
||||
['priceUnit', '单价单位'],
|
||||
['quantity', '数量'],
|
||||
['quantityUnit', '数量单位'],
|
||||
];
|
||||
const emptyField = fields.find(
|
||||
@@ -590,10 +593,10 @@ export default {
|
||||
if (!this.validateRoutePhones(route)) return;
|
||||
if (route.documentType === '运单') {
|
||||
if (this.isRoad(route)) {
|
||||
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo || !route.mileage)) return this.$message.warning('请填写承运商、车牌号和里程');
|
||||
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo || !route.trailerVehicleNo || !route.escortName || !route.escortPhone || route.mileage === undefined || route.mileage === null || route.mileage === '')) return this.$message.warning('请补全自运或网货平台的车辆与人员信息');
|
||||
} else if (!route.vehicleNo || !route.captainName || !route.driverPhone || !route.containerNo || !route.cabinNo || route.mileage === undefined || route.mileage === null || route.mileage === '' || (route.carrierType === '承运商' && !route.carrierName)) {
|
||||
return this.$message.warning('请补全非公路运输的承运信息和里程');
|
||||
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo)) return this.$message.warning('请填写承运商和车牌号');
|
||||
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo || !route.trailerVehicleNo || !route.escortName || !route.escortPhone)) return this.$message.warning('请补全自运或网货平台的车辆与人员信息');
|
||||
} else if (!route.vehicleNo || !route.captainName || !route.driverPhone || !route.containerNo || !route.cabinNo || (route.carrierType === '承运商' && !route.carrierName)) {
|
||||
return this.$message.warning('请补全非公路运输的承运信息');
|
||||
}
|
||||
}
|
||||
const batchNo = `${route.segmentNo}-${Date.now()}`;
|
||||
@@ -666,9 +669,13 @@ export default {
|
||||
.pending-list-spacer { height: min(436px, calc(100vh - 130px)); }
|
||||
.master-overview, .dispatch-section { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
||||
.overview-heading, .segment-header, .pending-bar { display: flex; align-items: center; justify-content: space-between; }
|
||||
.segment-header :deep(.el-checkbox.is-checked .el-checkbox__label) { font-size: 16px; font-weight: 700; }
|
||||
.segment-checkbox-label { display: inline-flex; align-items: center; min-width: 0; }
|
||||
.segment-checkbox-path { display: inline-block; white-space: nowrap; }
|
||||
.overview-heading { padding: 18px 24px 12px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
|
||||
.overview-content { display: grid; grid-template-columns: minmax(400px, .9fr) minmax(620px, 1.6fr); gap: 28px; padding: 8px 24px 20px; }
|
||||
.overview-meta { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 24px; margin: 0; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
|
||||
.overview-meta dd.overview-meta__plain-value { color: #303133; }
|
||||
.route-overview { display: flex; align-items: flex-start; justify-content: flex-end; padding-top: 8px; overflow-x: auto; }
|
||||
.route-overview__node { display: grid; flex: 0 0 130px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
|
||||
.route-badge { display: inline-flex; width: 32px; height: 32px; align-items: center; justify-content: center; border-radius: 4px; background: #67c23a; color: #fff; font-weight: 600; &.start { background: #409eff; } &.end { background: #e6a23c; } }
|
||||
@@ -679,7 +686,7 @@ export default {
|
||||
.transport-type-field :deep(.el-input) { width: 240px; }
|
||||
.goods-heading { display: flex; align-items: center; justify-content: space-between; margin: 0 -24px 12px; padding: 14px 24px; border-top: 1px solid #eff1f7; border-bottom: 1px solid #eff1f7; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } span { color: #909399; font-size: 13px; } }
|
||||
.freight-heading { margin: 16px 0 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
||||
.goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } :deep(.goods-table__remaining .cell) { white-space: nowrap; } :deep(.goods-table__dispatch .el-input) { width: 100%; min-width: 0; } }
|
||||
.goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } :deep(.goods-table__remaining .cell) { white-space: nowrap; } :deep(.goods-table__dispatch .el-input) { width: 100%; min-width: 0; } :deep(.goods-table__cargo-type .el-input__inner), :deep(.goods-table__cargo-name .el-select__selected-item) { color: #303133; } }
|
||||
.freight-form { :deep(.freight-unit-select) { width: 92px; } }
|
||||
.carrier-type-form { margin-top: 16px; }
|
||||
.carrier-form { padding-top: 8px; }
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="发货地址" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-model="form.departureName" class="route-address-name" readonly placeholder="发货地址" @click="openAddressMap('departure')" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly placeholder="请输入详细地址" @click="openAddressMap('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" @click="openCommonAddress('departure')" /></el-tooltip></div
|
||||
><el-cascader v-model="addressRegionPaths.departure" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('departure', value)" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly placeholder="请输入详细地址" @click="openAddressMap('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" @click="openCommonAddress('departure')" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人"
|
||||
><el-input v-model="form.departureContact" placeholder="请输入"
|
||||
@@ -74,9 +74,9 @@
|
||||
<el-form-item label="计划开始日期"
|
||||
><el-date-picker
|
||||
v-model="form.planStartTime"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
/></el-form-item>
|
||||
</div>
|
||||
@@ -84,12 +84,12 @@
|
||||
</el-step>
|
||||
<el-step v-for="(route, index) in form.routes" :key="index">
|
||||
<template #icon><span class="route-step-icon route-step-icon--middle">经</span></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ index + 1 }}</span><span class="route-title-path">{{ getRouteTitle(index).replace(`段${index + 1}:`, '') }}</span><div class="route-transport-control"><span>运输方式</span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ index + 1 }}</span><el-tooltip :content="getRouteTitle(index).replace(`段${index + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getRouteTitle(index), index + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式</span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #description>
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="途经地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-model="route.departureName" class="route-address-name" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" @click="openAddressMap(`route-${index}`)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" @click="openAddressMap(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
><el-cascader v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!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="!route.transportType || !isRoadTransportType(route.transportType)" placeholder="请先选择运输方式" @click="openAddressMap(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item>
|
||||
<el-form-item label="联系方式"><div class="route-contact-actions"><el-input v-model="route.departurePhone" placeholder="请输入" /><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></div></el-form-item>
|
||||
@@ -98,12 +98,12 @@
|
||||
</el-step>
|
||||
<el-step>
|
||||
<template #icon><span class="route-step-icon route-step-icon--end">终</span></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ form.routes.length + 1 }}</span><span class="route-title-path">{{ getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '') }}</span><div class="route-transport-control"><span>运输方式</span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ form.routes.length + 1 }}</span><el-tooltip :content="getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getFinalRouteTitle(), form.routes.length + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式</span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #description>
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="收货地址" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-model="form.arrivalName" class="route-address-name" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" @click="openAddressMap('arrival')" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" @click="openAddressMap('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
><el-cascader v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!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="!form.finalTransportType || !isRoadTransportType(form.finalTransportType)" placeholder="请选择省市区" @click="openAddressMap('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人"
|
||||
><el-input v-model="form.arrivalContact" placeholder="请输入" /></el-form-item>
|
||||
@@ -112,9 +112,9 @@
|
||||
<el-form-item label="计划结束日期"
|
||||
><el-date-picker
|
||||
v-model="form.planEndTime"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
/></el-form-item>
|
||||
</div>
|
||||
@@ -299,6 +299,31 @@
|
||||
width="1100px"
|
||||
@opened="loadAddressList"
|
||||
>
|
||||
<div class="route-dialog__search common-address-dialog__search">
|
||||
<el-form :model="addressQuery" label-position="right" label-width="auto">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="地址名称">
|
||||
<el-input v-model="addressQuery.addressName" clearable placeholder="请输入" @keyup.enter="searchAddressList" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="详细地址">
|
||||
<el-input v-model="addressQuery.detailAddress" clearable placeholder="请输入" @keyup.enter="searchAddressList" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="行政区划">
|
||||
<el-input v-model="addressQuery.regionName" clearable placeholder="请输入" @keyup.enter="searchAddressList" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6" class="route-dialog__search-actions">
|
||||
<el-button type="primary" @click="searchAddressList">查询</el-button>
|
||||
<el-button @click="resetAddressList">重置</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table v-loading="addressLoading" :data="addressRows" border height="360">
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column prop="addressName" label="地址名称" min-width="150" />
|
||||
@@ -386,10 +411,22 @@
|
||||
<div class="address-pagination"><el-pagination v-model:current-page="stationPage.current" v-model:page-size="stationPage.size" :page-sizes="[10, 20, 50]" layout="total, prev, pager, next, sizes" :total="stationPage.total" @size-change="loadStationList" @current-change="loadStationList" /></div>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="commonCargoVisible" title="选择常用货物" append-to-body width="1100px" @opened="loadCommonCargoList">
|
||||
<el-table v-loading="commonCargoLoading" :data="commonCargoRows" border height="360">
|
||||
<div class="route-dialog__search common-cargo-dialog__search">
|
||||
<el-form :model="commonCargoQuery" label-position="right" label-width="86px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6"><el-form-item label="货物名称"><el-input v-model="commonCargoQuery.cargoName" clearable placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="货物类型"><el-cascader v-model="commonCargoQuery.cargoTypePath" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" clearable filterable placeholder="请选择" :loading="cargoTypeLoading" @visible-change="visible => visible && loadCargoTypeOptions()" @change="handleCommonCargoTypeChange" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="规格型号"><el-input v-model="commonCargoQuery.specificationModel" clearable placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="6" class="route-dialog__search-actions"><el-button type="primary" @click="searchCommonCargoList">查询</el-button><el-button @click="resetCommonCargoList">重置</el-button></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table v-loading="commonCargoLoading" :data="commonCargoRows" border height="360" @selection-change="handleCommonCargoSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" fixed="left" />
|
||||
<el-table-column type="index" label="序号" width="70" /><el-table-column prop="cargoName" label="货物名称" min-width="150" /><el-table-column prop="cargoCode" label="货物编号" min-width="130" /><el-table-column prop="firstCargoTypeName" label="一级货物类型" min-width="150" /><el-table-column prop="secondCargoTypeName" label="二级货物类型" min-width="150" /><el-table-column prop="packageType" label="包装" min-width="100" /><el-table-column prop="brand" label="品牌" min-width="120" /><el-table-column prop="specification" label="规格" min-width="120" /><el-table-column prop="model" label="型号" min-width="120" /><el-table-column label="操作" width="100" fixed="right"><template #default="{ row }"><el-link type="primary" @click="selectCommonCargo(row)">选择</el-link></template></el-table-column>
|
||||
</el-table>
|
||||
<div class="address-pagination"><el-pagination v-model:current-page="commonCargoPage.current" v-model:page-size="commonCargoPage.size" :page-sizes="[10, 20, 50]" layout="total, prev, pager, next, sizes" :total="commonCargoPage.total" @size-change="loadCommonCargoList" @current-change="loadCommonCargoList" /></div>
|
||||
<template #footer><el-button @click="commonCargoVisible = false">关闭</el-button><el-button type="primary" :disabled="!commonCargoSelected.length" @click="confirmCommonCargoSelection">确定</el-button></template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="cargoImportVisible" title="导入货物" append-to-body width="560px"><div class="cargo-import"><el-upload action="#" accept=".xls,.xlsx" :auto-upload="false" :show-file-list="false" :disabled="cargoImportLoading" :on-change="handleCargoImport"><el-button type="primary" :loading="cargoImportLoading">上传</el-button><template #tip><div class="el-upload__tip">支持 .xlsx、.xls 文件,单个文件不超过 50M</div></template></el-upload><el-button type="primary" plain @click="downloadCargoTemplate">点击下载模板</el-button></div><template #footer><el-button @click="cargoImportVisible = false">关闭</el-button></template></el-dialog>
|
||||
<el-dialog v-model="routeDialogVisible" title="选择线路" width="1280px" @opened="loadRouteList">
|
||||
@@ -485,6 +522,7 @@ import { getList as getAirportMasterList } from '@/api/base/airport-master';
|
||||
import { getList as getPortTerminalList } from '@/api/base/port-terminal';
|
||||
import { getList as getRailwayStationList } from '@/api/base/railway-station';
|
||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||
import { exportBlob, importBlob } from '@/api/common';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
@@ -534,7 +572,12 @@ export default {
|
||||
addressLoading: false,
|
||||
addressTarget: '',
|
||||
addressRows: [],
|
||||
addressQuery: {},
|
||||
addressPage: { current: 1, size: 10, total: 0 },
|
||||
addressRegionPaths: { departure: [], arrival: [] },
|
||||
regionOptions: [],
|
||||
regionLoading: false,
|
||||
regionRequest: null,
|
||||
stationDialogVisible: false,
|
||||
stationLoading: false,
|
||||
stationQuery: {},
|
||||
@@ -552,6 +595,8 @@ export default {
|
||||
commonCargoVisible: false,
|
||||
commonCargoLoading: false,
|
||||
commonCargoRows: [],
|
||||
commonCargoQuery: {},
|
||||
commonCargoSelected: [],
|
||||
commonCargoPage: { current: 1, size: 10, total: 0 },
|
||||
cargoTypeLoading: false,
|
||||
cargoTypeOptions: [],
|
||||
@@ -625,11 +670,19 @@ export default {
|
||||
emitPath: true,
|
||||
};
|
||||
},
|
||||
regionCascaderProps() {
|
||||
return {
|
||||
value: 'id',
|
||||
label: 'title',
|
||||
children: 'children',
|
||||
emitPath: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions()]);
|
||||
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions(), this.loadRegionOptions()]);
|
||||
if (this.id) {
|
||||
await this.loadDetail(this.id);
|
||||
}
|
||||
@@ -641,6 +694,118 @@ export default {
|
||||
responseData(response) {
|
||||
return response?.data?.data || response?.data || response || {};
|
||||
},
|
||||
async loadRegionOptions() {
|
||||
if (this.regionOptions.length) return this.regionOptions;
|
||||
if (this.regionRequest) return this.regionRequest;
|
||||
this.regionLoading = true;
|
||||
this.regionRequest = getRegionLazyTree()
|
||||
.then(response => {
|
||||
const data = response?.data?.data || response?.data || response || [];
|
||||
this.regionOptions = this.extractChinaRegionOptions(this.buildRegionTree(data));
|
||||
this.syncAddressRegionPaths();
|
||||
return this.regionOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.regionLoading = false;
|
||||
this.regionRequest = null;
|
||||
});
|
||||
return this.regionRequest;
|
||||
},
|
||||
buildRegionTree(regions = []) {
|
||||
const flat = [];
|
||||
const collect = list =>
|
||||
(list || []).forEach(item => {
|
||||
flat.push({ ...item, children: undefined });
|
||||
if (item.children?.length) collect(item.children);
|
||||
});
|
||||
collect(regions);
|
||||
const nodeMap = new Map();
|
||||
flat.forEach(item => {
|
||||
const id = item.id ?? item.code ?? item.value;
|
||||
if (id === undefined || id === null) return;
|
||||
nodeMap.set(String(id), {
|
||||
...item,
|
||||
id: String(id),
|
||||
parentId: String(item.parentId ?? item.parentCode ?? ''),
|
||||
children: [],
|
||||
});
|
||||
});
|
||||
const roots = [];
|
||||
nodeMap.forEach(node => {
|
||||
const parent = nodeMap.get(node.parentId);
|
||||
if (parent && parent !== node) parent.children.push(node);
|
||||
else roots.push(node);
|
||||
});
|
||||
const normalize = node => {
|
||||
const children = node.children.map(item => normalize(item));
|
||||
return { ...node, title: node.title || node.name || '', children: children.length ? children : undefined, leaf: !children.length };
|
||||
};
|
||||
return roots.map(item => normalize(item));
|
||||
},
|
||||
extractChinaRegionOptions(regions = []) {
|
||||
const china = regions.find(item => ['中国', '中华人民共和国'].includes(item.title));
|
||||
if (china?.children?.length) return china.children;
|
||||
if (regions.length === 1 && regions[0].children?.length) return regions[0].children;
|
||||
return regions;
|
||||
},
|
||||
findRegionPathByName(options = [], regionName = '', parents = []) {
|
||||
const target = String(regionName || '').replace(/\s+/g, '');
|
||||
if (!target) return [];
|
||||
for (const item of options) {
|
||||
const nextParents = [...parents, item];
|
||||
const name = nextParents.map(region => region.title || region.name || '').join('');
|
||||
if (name === target) return nextParents.map(region => region.id);
|
||||
const path = this.findRegionPathByName(item.children, target, nextParents);
|
||||
if (path.length) return path;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
findRegionPathByCode(options = [], regionCode = '', parents = []) {
|
||||
const target = String(regionCode || '').trim();
|
||||
if (!target) return [];
|
||||
for (const item of options) {
|
||||
const nextParents = [...parents, item];
|
||||
if (String(item.id) === target) return nextParents.map(region => region.id);
|
||||
const path = this.findRegionPathByCode(item.children, target, nextParents);
|
||||
if (path.length) return path;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
regionLabels(path = []) {
|
||||
let options = this.regionOptions;
|
||||
const labels = [];
|
||||
path.forEach(value => {
|
||||
const option = options.find(item => String(item.id) === String(value));
|
||||
if (!option) return;
|
||||
labels.push(option.title || option.name || '');
|
||||
options = option.children || [];
|
||||
});
|
||||
return labels;
|
||||
},
|
||||
handleAddressRegionChange(target, value) {
|
||||
const field = this.addressModel(target);
|
||||
const path = Array.isArray(value) ? value.map(item => String(item)) : [];
|
||||
this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path };
|
||||
if (!field) return;
|
||||
const labels = this.regionLabels(path);
|
||||
field.model[field.name] = labels.join('');
|
||||
field.model[field.regionCode] = path.at(-1) || '';
|
||||
},
|
||||
syncAddressRegionPath(target) {
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return;
|
||||
const codePath = this.findRegionPathByCode(this.regionOptions, field.model[field.regionCode]);
|
||||
const path = codePath.length
|
||||
? codePath
|
||||
: this.findRegionPathByName(this.regionOptions, field.model[field.name]);
|
||||
this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path };
|
||||
},
|
||||
syncAddressRegionPaths() {
|
||||
if (!this.regionOptions.length) return;
|
||||
['departure', 'arrival', ...this.form.routes.map((route, index) => `route-${index}`)].forEach(target => {
|
||||
this.syncAddressRegionPath(target);
|
||||
});
|
||||
},
|
||||
async loadDetail(id) {
|
||||
const res = await api.getDetail(id);
|
||||
const detail = this.responseData(res);
|
||||
@@ -652,6 +817,8 @@ export default {
|
||||
finalSegment.departureName === detail.arrivalName;
|
||||
this.form = {
|
||||
...detail,
|
||||
planStartTime: this.normalizeDateValue(detail.planStartTime),
|
||||
planEndTime: this.normalizeDateValue(detail.planEndTime),
|
||||
routes: hasFinalSegment ? routes.slice(0, -1) : routes.length ? routes : [this.createRoute(0)],
|
||||
finalTransportType: hasFinalSegment
|
||||
? finalSegment.transportType
|
||||
@@ -660,6 +827,7 @@ export default {
|
||||
attachmentsJson: detail.attachmentsJson || '[]',
|
||||
transportOrganizationType: '多式联运',
|
||||
};
|
||||
this.syncAddressRegionPaths();
|
||||
this.restoreGoodsCargoTypePaths();
|
||||
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
|
||||
this.selectedAttachmentRows = [];
|
||||
@@ -675,17 +843,54 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
normalizeDateValue(value) {
|
||||
if (!value) return '';
|
||||
if (value instanceof Date) {
|
||||
const year = value.getFullYear();
|
||||
const month = String(value.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(value.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
return String(value).replace('T', ' ').slice(0, 10);
|
||||
},
|
||||
getRegionDisplay(target, fallback) {
|
||||
const labels = this.regionLabels(this.addressRegionPaths[target] || []);
|
||||
return labels.length ? labels.join('/') : fallback;
|
||||
},
|
||||
getRouteTitle(index) {
|
||||
const departureName =
|
||||
index === 0
|
||||
? this.form.departureName || '发货地址'
|
||||
: this.form.routes[index - 1].departureName || '途经地';
|
||||
return `段${index + 1}:${departureName} - ${this.form.routes[index].departureName || '途经地'}`;
|
||||
? this.getRegionDisplay('departure', this.form.departureName || '发货地址')
|
||||
: this.getRegionDisplay(
|
||||
`route-${index - 1}`,
|
||||
this.form.routes[index - 1].departureName || '途经地'
|
||||
);
|
||||
const arrivalName = this.getRegionDisplay(
|
||||
`route-${index}`,
|
||||
this.form.routes[index].departureName || '途经地'
|
||||
);
|
||||
return `段${index + 1}:${departureName} → ${arrivalName}`;
|
||||
},
|
||||
truncateRouteAddress(value) {
|
||||
const text = String(value || '');
|
||||
return text.length > 6 ? `${text.slice(0, 6)}...` : text;
|
||||
},
|
||||
formatRouteTitle(title, segmentNo) {
|
||||
const fullTitle = String(title || '').replace(`段${segmentNo}:`, '');
|
||||
const separator = ' → ';
|
||||
const separatorIndex = fullTitle.indexOf(separator);
|
||||
if (separatorIndex < 0) return this.truncateRouteAddress(fullTitle);
|
||||
const departure = fullTitle.slice(0, separatorIndex);
|
||||
const arrival = fullTitle.slice(separatorIndex + separator.length);
|
||||
return `${this.truncateRouteAddress(departure)}${separator}${this.truncateRouteAddress(arrival)}`;
|
||||
},
|
||||
getFinalRouteTitle() {
|
||||
return `段${this.form.routes.length + 1}:${this.lastRouteName} - ${
|
||||
this.form.arrivalName || '收货地址'
|
||||
}`;
|
||||
const departureName = this.getRegionDisplay(
|
||||
`route-${this.form.routes.length - 1}`,
|
||||
this.lastRouteName
|
||||
);
|
||||
const arrivalName = this.getRegionDisplay('arrival', this.form.arrivalName || '收货地址');
|
||||
return `段${this.form.routes.length + 1}:${departureName} → ${arrivalName}`;
|
||||
},
|
||||
responseRecords(response) {
|
||||
const data = response?.data?.data || response?.data || response;
|
||||
@@ -730,6 +935,7 @@ export default {
|
||||
phone: 'departurePhone',
|
||||
longitude: 'departureLongitude',
|
||||
latitude: 'departureLatitude',
|
||||
regionCode: 'departureRegionCode',
|
||||
siteCode: 'departureSiteCode',
|
||||
}
|
||||
: null;
|
||||
@@ -743,6 +949,7 @@ export default {
|
||||
phone: `${prefix}Phone`,
|
||||
longitude: `${prefix}Longitude`,
|
||||
latitude: `${prefix}Latitude`,
|
||||
regionCode: `${prefix}RegionCode`,
|
||||
siteCode: `${prefix}SiteCode`,
|
||||
};
|
||||
},
|
||||
@@ -759,6 +966,10 @@ export default {
|
||||
if (type.includes('航空') || type.includes('空运')) return 'air';
|
||||
return 'road';
|
||||
},
|
||||
isRoadTransportType(type) {
|
||||
const value = String(type || '').trim().toLowerCase();
|
||||
return value === 'road' || value.includes('公路');
|
||||
},
|
||||
openCommonAddress(target) {
|
||||
this.addressTarget = target;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
@@ -767,6 +978,7 @@ export default {
|
||||
this.stationDialogVisible = true;
|
||||
return;
|
||||
}
|
||||
this.addressQuery = {};
|
||||
this.addressPage.current = 1;
|
||||
this.addressDialogVisible = true;
|
||||
},
|
||||
@@ -814,12 +1026,14 @@ export default {
|
||||
const target = this.addressModel(this.addressTarget);
|
||||
if (!target) return;
|
||||
const station = this.normalizeStation(row);
|
||||
target.model[target.name] = station.stationName;
|
||||
target.model[target.name] = station.regionName || station.stationName;
|
||||
target.model[target.address] = station.detailAddress;
|
||||
target.model[target.longitude] = station.longitude || '';
|
||||
target.model[target.latitude] = station.latitude || '';
|
||||
target.model[target.regionCode] = station.regionCode || '';
|
||||
target.model[target.siteCode] = station.stationCode;
|
||||
this.stationDialogVisible = false;
|
||||
this.syncAddressRegionPath(this.addressTarget);
|
||||
},
|
||||
async loadAddressList() {
|
||||
this.addressLoading = true;
|
||||
@@ -827,7 +1041,7 @@ export default {
|
||||
const response = await getCommonAddressList(
|
||||
this.addressPage.current,
|
||||
this.addressPage.size,
|
||||
{ allDept: 0 }
|
||||
{ ...this.addressQuery, allDept: 0 }
|
||||
);
|
||||
const data = response?.data?.data || response?.data || response || {};
|
||||
this.addressRows = data.records || [];
|
||||
@@ -836,18 +1050,33 @@ export default {
|
||||
this.addressLoading = false;
|
||||
}
|
||||
},
|
||||
searchAddressList() {
|
||||
this.addressPage.current = 1;
|
||||
this.loadAddressList();
|
||||
},
|
||||
resetAddressList() {
|
||||
this.addressQuery = {};
|
||||
this.addressPage.current = 1;
|
||||
this.loadAddressList();
|
||||
},
|
||||
selectAddress(row) {
|
||||
const target = this.addressModel(this.addressTarget);
|
||||
if (!target) return;
|
||||
target.model[target.name] = row.addressName || row.regionName || '';
|
||||
target.model[target.name] = row.regionName || row.addressName || '';
|
||||
target.model[target.address] = row.detailAddress || row.address || '';
|
||||
target.model[target.contact] = row.contactName || target.model[target.contact] || '';
|
||||
target.model[target.phone] = row.contactPhone || target.model[target.phone] || '';
|
||||
target.model[target.longitude] = row.longitude || '';
|
||||
target.model[target.latitude] = row.latitude || '';
|
||||
target.model[target.regionCode] = row.regionCode || '';
|
||||
this.addressDialogVisible = false;
|
||||
this.syncAddressRegionPath(this.addressTarget);
|
||||
},
|
||||
openAddressMap(target) {
|
||||
if ((target.startsWith('route-') || target === 'arrival') && !this.isRoadTransportType(this.addressTransportType(target))) {
|
||||
this.$message.info('非公路运输地址不能使用地图选择');
|
||||
return;
|
||||
}
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return;
|
||||
this.mapTarget = target;
|
||||
@@ -855,6 +1084,7 @@ export default {
|
||||
this.mapSelected = {
|
||||
longitude: field.model[field.longitude] || '',
|
||||
latitude: field.model[field.latitude] || '',
|
||||
regionCode: field.model[field.regionCode] || '',
|
||||
detailAddress: this.mapKeyword,
|
||||
regionName: field.model[field.name] || '',
|
||||
};
|
||||
@@ -935,6 +1165,7 @@ export default {
|
||||
latitude,
|
||||
detailAddress: keyword || this.mapKeyword,
|
||||
regionName: '',
|
||||
regionCode: '',
|
||||
};
|
||||
this.renderAddressMarker([longitude, latitude]);
|
||||
this.mapStatus = '已选点,可确认回填';
|
||||
@@ -947,6 +1178,7 @@ export default {
|
||||
this.mapSelected.regionName = [component.province, component.city, component.district]
|
||||
.filter(Boolean)
|
||||
.join('');
|
||||
this.mapSelected.regionCode = component.adcode || '';
|
||||
this.mapKeyword = this.mapSelected.detailAddress;
|
||||
this.mapStatus = '已解析地址,可确认回填';
|
||||
}
|
||||
@@ -967,16 +1199,22 @@ export default {
|
||||
target.model[target.address] = this.mapSelected.detailAddress || this.mapKeyword;
|
||||
target.model[target.longitude] = this.mapSelected.longitude;
|
||||
target.model[target.latitude] = this.mapSelected.latitude;
|
||||
target.model[target.regionCode] = this.mapSelected.regionCode || '';
|
||||
this.mapDialogVisible = false;
|
||||
this.syncAddressRegionPath(this.mapTarget);
|
||||
},
|
||||
openCommonCargoDialog() {
|
||||
this.commonCargoQuery = {};
|
||||
this.commonCargoSelected = [];
|
||||
this.commonCargoPage.current = 1;
|
||||
this.commonCargoVisible = true;
|
||||
},
|
||||
async loadCommonCargoList() {
|
||||
this.commonCargoSelected = [];
|
||||
this.commonCargoLoading = true;
|
||||
try {
|
||||
const response = await getCommonCargoList(this.commonCargoPage.current, this.commonCargoPage.size, { allDept: 0 });
|
||||
const { cargoTypePath, ...query } = this.commonCargoQuery;
|
||||
const response = await getCommonCargoList(this.commonCargoPage.current, this.commonCargoPage.size, { ...query, allDept: 0 });
|
||||
const data = response?.data?.data || response?.data || response || {};
|
||||
this.commonCargoRows = data.records || [];
|
||||
this.commonCargoPage.total = data.total || 0;
|
||||
@@ -984,6 +1222,31 @@ export default {
|
||||
this.commonCargoLoading = false;
|
||||
}
|
||||
},
|
||||
searchCommonCargoList() {
|
||||
this.commonCargoPage.current = 1;
|
||||
this.loadCommonCargoList();
|
||||
},
|
||||
resetCommonCargoList() {
|
||||
this.commonCargoQuery = {};
|
||||
this.commonCargoPage.current = 1;
|
||||
this.loadCommonCargoList();
|
||||
},
|
||||
handleCommonCargoTypeChange(value) {
|
||||
const path = Array.isArray(value) ? value.map(item => String(item)) : [];
|
||||
const firstCargoType = this.getCargoTypeByPath(path.slice(0, 1));
|
||||
const secondCargoType = path.length > 1 ? this.getCargoTypeByPath(path) : null;
|
||||
this.commonCargoQuery = {
|
||||
...this.commonCargoQuery,
|
||||
cargoTypePath: path,
|
||||
firstCargoTypeName: firstCargoType?.label || '',
|
||||
firstCargoTypeCode: firstCargoType?.cargoCode || firstCargoType?.code || '',
|
||||
secondCargoTypeName: secondCargoType?.label || '',
|
||||
secondCargoTypeCode: secondCargoType?.cargoCode || secondCargoType?.code || '',
|
||||
};
|
||||
},
|
||||
handleCommonCargoSelectionChange(rows) {
|
||||
this.commonCargoSelected = rows || [];
|
||||
},
|
||||
async loadCargoTypeOptions() {
|
||||
if (this.cargoTypeOptions.length || this.cargoTypeLoading) return;
|
||||
this.cargoTypeLoading = true;
|
||||
@@ -1133,7 +1396,16 @@ export default {
|
||||
});
|
||||
},
|
||||
selectCommonCargo(row) {
|
||||
const cargo = this.normalizeCargoRow(row, this.resolveCargoTypePath(row));
|
||||
this.appendCommonCargoRows([row]);
|
||||
this.commonCargoVisible = false;
|
||||
},
|
||||
confirmCommonCargoSelection() {
|
||||
if (!this.commonCargoSelected.length) return;
|
||||
this.appendCommonCargoRows(this.commonCargoSelected);
|
||||
this.$message.success(`成功选择${this.commonCargoSelected.length}条常用货物`);
|
||||
this.commonCargoVisible = false;
|
||||
},
|
||||
appendCommonCargoRows(rows = []) {
|
||||
const firstGoods = this.form.goods[0];
|
||||
const isFirstGoodsEmpty =
|
||||
firstGoods &&
|
||||
@@ -1148,12 +1420,11 @@ export default {
|
||||
'remark',
|
||||
].some(key => firstGoods[key]);
|
||||
|
||||
if (isFirstGoodsEmpty) {
|
||||
this.form.goods.splice(0, 1, cargo);
|
||||
} else {
|
||||
this.form.goods.push(cargo);
|
||||
}
|
||||
this.commonCargoVisible = false;
|
||||
rows.forEach((row, index) => {
|
||||
const cargo = this.normalizeCargoRow(row, this.resolveCargoTypePath(row));
|
||||
if (isFirstGoodsEmpty && index === 0) this.form.goods.splice(0, 1, cargo);
|
||||
else this.form.goods.push(cargo);
|
||||
});
|
||||
},
|
||||
resolveCargoTypePath(row = {}) {
|
||||
return (
|
||||
@@ -1275,6 +1546,7 @@ export default {
|
||||
this.form.arrivalAddress = route.arrivalAddress || '';
|
||||
this.form.arrivalContact = route.arrivalContact || '';
|
||||
this.form.arrivalPhone = route.arrivalPhone || '';
|
||||
this.syncAddressRegionPaths();
|
||||
this.form.finalTransportType = '公路运输';
|
||||
if (!this.form.routes.length) this.addRoute();
|
||||
this.routeDialogVisible = false;
|
||||
@@ -1287,6 +1559,7 @@ export default {
|
||||
},
|
||||
removeRoute(index) {
|
||||
this.form.routes.splice(index, 1);
|
||||
this.syncAddressRegionPaths();
|
||||
},
|
||||
addGoods(index) {
|
||||
this.form.goods.splice(index + 1, 0, { quantityUnit: quantityUnitOptions[0] });
|
||||
@@ -1416,6 +1689,8 @@ export default {
|
||||
}
|
||||
const payload = {
|
||||
...this.form,
|
||||
planStartTime: this.normalizeDateValue(this.form.planStartTime),
|
||||
planEndTime: this.normalizeDateValue(this.form.planEndTime),
|
||||
attachmentsJson: JSON.stringify(this.attachmentRows),
|
||||
routes: [
|
||||
...this.form.routes.map((route, index) => ({ ...route, segmentNo: `段${index + 1}` })),
|
||||
@@ -1537,6 +1812,9 @@ export default {
|
||||
padding: 12px 12px 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.common-cargo-dialog__search {
|
||||
background: #fff;
|
||||
}
|
||||
.route-dialog__search-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -1638,9 +1916,14 @@ export default {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.route-title-path {
|
||||
display: inline-block;
|
||||
max-width: 24em;
|
||||
overflow: hidden;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.route-transport-control {
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<basic-container class="contract-change-page">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="auto">
|
||||
<section class="change-section">
|
||||
<div class="dialog-section-title">基本信息</div>
|
||||
<el-row :gutter="28">
|
||||
<el-col :span="24"><el-form-item label="变更类型"><el-radio-group v-model="form.changeType"><el-radio label="合同信息变更" /><el-radio label="终止合同" /></el-radio-group></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="合同编号"><el-input v-model="form.contractNo" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="合同名称" prop="contractName"><el-input v-model="form.contractName" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="合同类型"><el-select v-model="form.contractCategory" disabled><el-option label="客户合同" value="客户合同" /><el-option label="承运商合同" value="承运商合同" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="甲方"><el-input v-model="form.partyA" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="乙方"><el-input v-model="form.partyB" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="所属项目"><el-input v-model="form.projectName" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="合同期限"><el-date-picker v-model="period" type="daterange" value-format="YYYY-MM-DD" range-separator="至" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="所属组织"><el-input v-model="form.organizationName" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="签订日期"><el-date-picker v-model="form.signDate" type="date" value-format="YYYY-MM-DD" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="合同格式"><el-select v-model="form.contractFormat"><el-option label="电子合同" value="电子合同" /><el-option label="纸质合同" value="纸质合同" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="结算方式"><el-input v-model="form.settlementMode" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="是否需要加盖法人章"><el-select v-model="form.legalSealFlag"><el-option label="是" :value="1" /><el-option label="否" :value="0" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="一式"><div class="inline-field"><el-input-number v-model="form.copyCount" :min="1" controls-position="right" /><span>份</span></div></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="回款账期"><el-input-number v-model="form.paymentDays" :min="0" controls-position="right" /><span class="unit">天</span></el-form-item></el-col>
|
||||
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" maxlength="2000" show-word-limit /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</section>
|
||||
|
||||
<section class="change-section">
|
||||
<div class="dialog-section-title">合同文件</div>
|
||||
<div class="attachment-head"><vehicle-attachment-upload v-model="contractFileRows" :readonly="false" :file-types="attachmentFileTypes" :max-size="500" :show-tip="false" :show-file-list="false" button-text="上传附件" @change="handleContractFileChange" /><el-button type="primary" :disabled="!contractFileRows.length" @click="handleContractFileBatchDownload">批量下载</el-button></div>
|
||||
<el-table :data="contractFileRows" border class="change-table" @selection-change="selectedContractFiles = $event"><el-table-column type="selection" width="55" /><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, contractFileRows)">{{ row.originalName || row.name }}</el-link></template></el-table-column><el-table-column label="文件大小" width="120"><template #default="{ row }">{{ formatFileSize(row.size) }}</template></el-table-column><el-table-column prop="uploadUserName" label="上传人" width="140" /><el-table-column prop="uploadTime" label="上传时间" width="170" /><el-table-column label="操作" width="100"><template #default="{ $index }"><el-link type="danger" @click="removeContractFile($index)">删除</el-link></template></el-table-column></el-table>
|
||||
</section>
|
||||
|
||||
<section class="change-section">
|
||||
<div class="section-head"><div class="dialog-section-title">计费信息</div><el-button type="primary" plain @click="addPlan">添加</el-button></div>
|
||||
<el-radio-group v-model="feeGenerationMode"><el-radio label="system">系统生成</el-radio><el-radio label="manual">手动生成</el-radio></el-radio-group>
|
||||
<el-table :data="plans" border class="change-table"><el-table-column type="index" label="序号" width="70" /><el-table-column prop="planName" label="方案名称" min-width="220" /><el-table-column label="默认方案" min-width="120"><template #default="{ row }">{{ row.defaultPlan ? '是' : '否' }}</template></el-table-column><el-table-column prop="remark" label="备注" min-width="260" /><el-table-column label="操作" width="180"><template #default="{ row, $index }"><el-link type="primary" @click="editPlan(row, $index)">编辑</el-link><el-link type="danger" @click="plans.splice($index, 1)">删除</el-link></template></el-table-column></el-table>
|
||||
</section>
|
||||
|
||||
<section class="change-section">
|
||||
<div class="dialog-section-title">结算单规则</div>
|
||||
<el-tabs v-model="settlementConfigTab">
|
||||
<el-tab-pane label="预结算配置" name="pre" />
|
||||
<el-tab-pane label="正式结算配置" name="formal" />
|
||||
</el-tabs>
|
||||
<div class="settlement-switch"><span>自动生成结算单</span><el-radio-group v-model="settlementRule.autoGenerate"><el-radio :label="1">开启</el-radio><el-radio :label="0">关闭</el-radio></el-radio-group></div>
|
||||
<el-form v-if="settlementRule.autoGenerate === 1" :model="settlementRule" label-position="right" label-width="auto" class="settlement-form">
|
||||
<el-form-item label="账单起始日期" required><el-date-picker v-model="settlementRule.billStartDate" type="date" placeholder="请选择账单起始日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="结算类型" required><el-select v-model="settlementRule.settlementType" placeholder="请选择结算类型" @change="handleSettlementTypeChange"><el-option v-for="item in settlementTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCycleType" label="账单周期类型" required><el-select v-model="settlementRule.billCycleType" placeholder="请选择账单周期类型" @change="handleCycleTypeChange"><el-option v-for="item in billCycleTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"><el-option v-for="item in billCutoffDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required><el-select v-model="settlementRule.cycleDays" placeholder="请选择周期天数"><el-option v-for="item in cycleDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="change-section">
|
||||
<div class="dialog-section-title">付款比例设置</div>
|
||||
<el-table :data="paymentRatioRows" border class="change-table">
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column prop="paymentTerm" label="付款笔数" min-width="180" />
|
||||
<el-table-column label="付款比例上限" min-width="220"><template #default="{ row }"><el-input-number v-model="row.ratioLimit" :min="0" :max="100" :precision="2" controls-position="right" /><span>%</span></template></el-table-column>
|
||||
<el-table-column label="备注" min-width="220"><template #default="{ row }"><el-input v-model="row.remark" maxlength="200" /></template></el-table-column>
|
||||
<el-table-column label="操作" width="100"><template #default="{ $index }"><el-link type="danger" @click="paymentRatioRows.splice($index, 1)">删除</el-link></template></el-table-column>
|
||||
</el-table>
|
||||
<el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button>
|
||||
</section>
|
||||
|
||||
<billing-plan-editor v-model="planDialogVisible" :value="planEditor" :index="planEditorIndex" @save="savePlan" />
|
||||
|
||||
<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-upload action="#" :auto-upload="false" multiple :show-file-list="false" @change="handleAttachment"><el-button plain>上传附件</el-button></el-upload>
|
||||
</section>
|
||||
|
||||
<section class="change-section change-reason-section"><div class="dialog-section-title">变更原因</div><el-form-item prop="changeReason"><el-input v-model="form.changeReason" type="textarea" :rows="4" maxlength="2000" show-word-limit placeholder="请输入变更原因" /></el-form-item><div class="dialog-section-title">变更材料</div><el-upload action="#" :auto-upload="false" multiple :show-file-list="false" @change="handleChangeMaterial"><el-button plain>上传变更材料</el-button></el-upload></section>
|
||||
<div class="page-footer"><el-button type="primary" @click="submit">提交</el-button><el-button @click="$router.back()">关闭</el-button></div>
|
||||
</el-form>
|
||||
|
||||
<el-dialog v-model="documentPreviewVisible" :title="previewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh">
|
||||
<open-file-viewer v-if="documentPreviewVisible && previewFile.url" :file="previewFile.url" :file-name="previewFile.name" :mime-type="previewFile.mimeType" width="100%" height="72vh" fit="contain" theme="auto" locale="zh-CN" :toolbar="viewerToolbar" :plugins="viewerPlugins" @unsupported="handlePreviewUnsupported" @error="handlePreviewError" />
|
||||
</el-dialog>
|
||||
<el-image-viewer v-if="imagePreviewVisible" :url-list="imagePreviewUrls" :initial-index="imagePreviewIndex" @close="imagePreviewVisible = false" />
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/contract-manage';
|
||||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||||
import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
|
||||
const viewerPlugins = [
|
||||
imagePlugin(),
|
||||
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
||||
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
|
||||
textPlugin(),
|
||||
fallbackPlugin(),
|
||||
];
|
||||
|
||||
export default {
|
||||
components: { BillingPlanEditor, ElImageViewer, OpenFileViewer },
|
||||
data() { return { form: {}, period: [], plans: [], attachments: [], contractFiles: [], contractFileRows: [], selectedContractFiles: [], paymentRatioRows: [], settlementConfigTab: 'pre', changeMaterials: [], imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { zoom: true, rotate: true, download: true, fullscreen: true }, feeGenerationMode: 'system', settlementRule: { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: '' }, preSettlementConfig: {}, formalSettlementConfig: {}, settlementTypeOptions: ['月结','日结','周结','半月结','固定天数周期结算'], billCycleTypeOptions: ['固定截单日','自然月'], billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({ label: `${index + 1}日`, value: index + 1 })), cycleDayOptions: [7,15,30,60].map(value => ({ label: `${value}天`, value })), planDialogVisible: false, planEditor: { planName: '', defaultPlan: false, remark: '', rules: [] }, planEditorIndex: -1, billingElements: ['按重量','按体积','按车辆','按里程','按吨·公里','固定金额(整单一口价)','按数量'], attachmentFileTypes: ['pdf','bmp','jpeg','png','jpg','doc','docx','ppt','pptx','xlsx','xls','eml','msg','zip'], rules: { contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }], changeReason: [{ required: true, message: '请输入变更原因', trigger: 'blur' }] } }; },
|
||||
computed: { showSettlementBillCycleType() { return this.settlementRule.settlementType === '月结'; }, showSettlementBillCutoffDay() { return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '固定截单日'; }, showSettlementCycleDays() { return this.settlementRule.settlementType === '固定天数周期结算'; } },
|
||||
mounted() { this.load(); },
|
||||
watch: { settlementConfigTab(tab, oldTab) { if (tab === oldTab) return; if (oldTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; this.settlementRule = { ...(tab === 'pre' ? this.preSettlementConfig : this.formalSettlementConfig) }; } },
|
||||
methods: {
|
||||
async load() { const id = this.$route.query.id; if (!id) return; const res = await api.getDetail(id); const data = res.data?.data || res.data || {}; this.form = { ...data, changeType: '合同信息变更' }; this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : []; this.plans = this.parse(data.billingPlanJson); this.attachments = this.parse(data.attachmentsJson); this.contractFileRows = this.parse(data.contractFileJson); const rules = this.parseObject(data.settlementRuleJson); const pre = this.parseObject(data.preSettlementConfigJson); const formal = this.parseObject(data.formalSettlementConfigJson); const legacy = Object.keys(rules).some(key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)) ? rules : {}; this.preSettlementConfig = rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy); this.formalSettlementConfig = rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy); this.settlementRule = { ...this.preSettlementConfig }; this.feeGenerationMode = data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system'); this.paymentRatioRows = this.parse(data.paymentRatioJson); },
|
||||
parse(value) { try { const result = JSON.parse(value || '[]'); return Array.isArray(result) ? result : []; } catch { return []; } },
|
||||
parseObject(value) { try { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15, ...(JSON.parse(value || '{}') || {}) }; } catch { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15 }; } },
|
||||
addPlan() { this.planEditorIndex = -1; this.planEditor = { planName: `计费方案${this.plans.length + 1}`, defaultPlan: !this.plans.length, remark: '', rules: [{}] }; this.planDialogVisible = true; },
|
||||
editPlan(row, index) { this.planEditorIndex = index; this.planEditor = JSON.parse(JSON.stringify(row)); this.planDialogVisible = true; },
|
||||
toggleDefaultPlan(value) { if (value) this.plans.forEach(item => { item.defaultPlan = false; }); },
|
||||
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) 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') })); },
|
||||
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() : ''; },
|
||||
isAttachmentImage(row) { return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(row)); },
|
||||
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
|
||||
handlePreviewUnsupported() { this.$message.warning('当前文件暂不支持在线预览'); },
|
||||
handlePreviewError() { this.$message.error('附件预览失败'); },
|
||||
removeContractFile(index) { this.contractFileRows.splice(index, 1); },
|
||||
handleContractFileBatchDownload() { (this.selectedContractFiles.length ? this.selectedContractFiles : this.contractFileRows).forEach(row => { if (row.url) window.open(row.url, '_blank'); }); },
|
||||
formatFileSize(value) { const size = Number(value || 0); return size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)}MB` : `${Math.max(1, Math.ceil(size / 1024))}KB`; },
|
||||
handleChangeMaterial(event) { if (event.raw) this.changeMaterials.push(event.raw); },
|
||||
addPaymentRatioRow() { this.paymentRatioRows.push({ paymentTerm: `第${this.paymentRatioRows.length + 1}笔`, ratioLimit: '', remark: '' }); },
|
||||
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeReason, changeReason: this.form.changeReason }); this.$message.success('变更已提交'); this.$router.back(); },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.contract-change-page { min-height: 100%; background: #fff; }
|
||||
.change-section { padding: 20px 24px; border-bottom: 1px solid #eff1f7; }
|
||||
.dialog-section-title { margin-bottom: 18px; font-size: 16px; font-weight: 600; }
|
||||
.dialog-section-title::before { display: inline-block; width: 4px; height: 16px; margin-right: 8px; vertical-align: -2px; background: #409eff; content: ''; }
|
||||
.section-head { display: flex; align-items: center; justify-content: space-between; }
|
||||
.change-table { margin: 16px 0; }
|
||||
.ratio-tip { margin-bottom: 8px; color: #f56c6c; }
|
||||
.unit { margin-left: 8px; }
|
||||
.inline-field { display: flex; align-items: center; gap: 8px; }
|
||||
.attachment-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
||||
.settlement-switch { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||
.settlement-form { display: grid; grid-template-columns: repeat(3, minmax(240px, 1fr)); gap: 8px 28px; }
|
||||
.change-reason-section { border: 1px dashed #ff8f9a; margin: 20px 16px; }
|
||||
.page-footer { display: flex; gap: 16px; padding: 20px 40px; border-top: 1px solid #eff1f7; }
|
||||
</style>
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="252" />
|
||||
<business-crud-page
|
||||
:api="api"
|
||||
:config="config"
|
||||
:crud-option="option"
|
||||
:menu-width="252"
|
||||
contract-form-page
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="loading-manage-page">
|
||||
<div class="loading-manage-page__search">
|
||||
<div v-if="!isStandaloneFormPage" class="loading-manage-page__search">
|
||||
<el-form :model="searchForm" label-position="right" label-width="88px">
|
||||
<div class="loading-manage-page__search-grid">
|
||||
<el-form-item label="配载单号">
|
||||
@@ -220,7 +220,7 @@
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="loading-manage-page__toolbar">
|
||||
<div v-if="!isStandaloneFormPage" class="loading-manage-page__toolbar">
|
||||
<div class="loading-manage-page__toolbar-left">
|
||||
<el-button type="primary" @click="openLoadingDialog('add')">新建配载</el-button>
|
||||
<el-button type="primary" plain @click="handleExport">批量导出</el-button>
|
||||
@@ -240,6 +240,7 @@
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-if="!isStandaloneFormPage"
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
@@ -260,7 +261,7 @@
|
||||
>{{ row.loadingNo }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="配载子单号" prop="loadingSubNos" min-width="220">
|
||||
<el-table-column label="运单号" prop="loadingSubNos" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<span class="loading-manage-page__sub-nos">
|
||||
<template v-if="splitText(row.loadingSubNos).length">
|
||||
@@ -280,14 +281,11 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="车牌号"
|
||||
prop="vehicleNo"
|
||||
min-width="170"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #header>
|
||||
<span class="loading-manage-page__vehicle-header">车牌号/航班号/船号<br />班列号</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
/>
|
||||
<el-table-column label="司机" prop="driverName" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="联系电话" prop="driverPhone" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip />
|
||||
@@ -357,7 +355,7 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="loading-manage-page__pagination">
|
||||
<div v-if="!isStandaloneFormPage" class="loading-manage-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.currentPage"
|
||||
v-model:page-size="page.pageSize"
|
||||
@@ -370,14 +368,24 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
width="96%"
|
||||
top="3vh"
|
||||
class="loading-manage-dialog"
|
||||
<component
|
||||
:is="isStandaloneFormPage ? 'div' : 'el-dialog'"
|
||||
:model-value="isStandaloneFormPage ? undefined : dialogVisible"
|
||||
:title="isStandaloneFormPage ? undefined : dialogTitle"
|
||||
:append-to-body="isStandaloneFormPage ? undefined : true"
|
||||
:destroy-on-close="isStandaloneFormPage ? undefined : true"
|
||||
:width="isStandaloneFormPage ? undefined : '96%'"
|
||||
:top="isStandaloneFormPage ? undefined : '3vh'"
|
||||
:class="[
|
||||
'loading-manage-dialog',
|
||||
{ 'loading-manage-form-dialog': isStandaloneFormPage },
|
||||
{ 'loading-manage-form-page': isStandaloneFormPage },
|
||||
]"
|
||||
:modal="isStandaloneFormPage ? undefined : true"
|
||||
:show-close="isStandaloneFormPage ? undefined : true"
|
||||
:close-on-click-modal="isStandaloneFormPage ? undefined : true"
|
||||
:close-on-press-escape="isStandaloneFormPage ? undefined : true"
|
||||
@update:model-value="value => !isStandaloneFormPage && (dialogVisible = value)"
|
||||
@closed="resetLoadingDialog"
|
||||
>
|
||||
<div v-loading="dialogLoading" class="loading-manage-dialog__body">
|
||||
@@ -396,7 +404,7 @@
|
||||
</section-card>
|
||||
<section-card>
|
||||
<template #title><span class="loading-detail__route-title">运输路线<el-link type="primary" @click="openRouteChangeDialog">变更运输路线</el-link></span></template>
|
||||
<div v-if="routeNodes.length" class="loading-detail__steps"><div v-for="(node, index) in routeNodes" :key="node.key || index" class="loading-detail__step"><div class="loading-detail__step-line"><span :class="['loading-detail__step-dot', index === 0 ? 'start' : index === routeNodes.length - 1 ? 'end' : 'middle']">{{ index === 0 ? '起' : index === routeNodes.length - 1 ? '终' : '经' }}</span></div><div class="loading-detail__step-address">{{ node.address }}</div><div class="loading-detail__step-tags"><el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag></div></div></div><el-empty v-else description="暂无路线" :image-size="60" />
|
||||
<div v-if="routeNodes.length" class="loading-detail__steps"><div v-for="(node, index) in routeNodes" :key="node.key || index" class="loading-detail__step"><div class="loading-detail__step-line"><span :class="['loading-detail__step-dot', index === 0 ? 'start' : index === routeNodes.length - 1 ? 'end' : 'middle']">{{ index === 0 ? '起' : index === routeNodes.length - 1 ? '终' : '经' }}</span></div><div class="loading-detail__step-address">{{ node.displayAddress || formatAddressText(node.address) }}</div><div class="loading-detail__step-tags"><el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag></div></div></div><el-empty v-else description="暂无路线" :image-size="60" />
|
||||
</section-card>
|
||||
<section-card title="货物信息"><el-table :data="cargoRows" border height="220"><el-table-column type="index" label="序号" width="70"/><el-table-column prop="projectName" label="项目名称" min-width="140"/><el-table-column prop="cargoName" label="货物名称" min-width="140"/><el-table-column prop="cargoType" label="货物类型" min-width="120"/><el-table-column prop="packageType" label="包装" min-width="100"/><el-table-column prop="weight" label="重量(吨)" min-width="110"/><el-table-column prop="volume" label="体积(方)" min-width="110"/><el-table-column prop="quantity" label="数量" min-width="90"/><el-table-column prop="materialCode" label="物料编码" min-width="120"/><el-table-column prop="equipmentCode" label="设备编码" min-width="120"/><el-table-column prop="brand" label="品牌" min-width="120"/><el-table-column prop="specificationModel" label="规格型号" min-width="140"/><el-table-column prop="remark" label="备注" min-width="160"/><el-table-column prop="unitPrice" label="货物单价(元)" min-width="140"/><el-table-column prop="planDate" label="计划日期" min-width="130"/></el-table></section-card>
|
||||
<section-card title="关联运单信息"><el-table :data="selectedWaybillRows" border><el-table-column type="index" label="序号" width="70"/><el-table-column prop="waybillNo" label="运单号" min-width="150"><template #default="{ row }"><el-link type="primary">{{ row.waybillNo || '-' }}</el-link></template></el-table-column><el-table-column label="配载单号" min-width="150"><template #default>{{ dialogForm.loadingNo || '-' }}</template></el-table-column><el-table-column prop="projectName" label="项目名称" min-width="150"/><el-table-column prop="customerName" label="客户" min-width="140"/><el-table-column prop="departureAddress" label="发货地址" min-width="220" show-overflow-tooltip/><el-table-column prop="arrivalAddress" label="到货地址" min-width="220" show-overflow-tooltip/></el-table></section-card>
|
||||
@@ -734,13 +742,21 @@
|
||||
label="发货地"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatWaybillAddress(row, 'departure') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="arrivalAddress"
|
||||
label="到货地"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatWaybillAddress(row, 'arrival') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-link
|
||||
@@ -766,7 +782,7 @@
|
||||
>
|
||||
<el-icon class="loading-manage-dialog__route-handle"><Rank /></el-icon>
|
||||
<span :class="['loading-manage-dialog__route-type', `loading-manage-dialog__route-type--${routeTypeClass(index)}`]">{{ routeTypeText(index) }}</span>
|
||||
<span class="loading-manage-dialog__route-address">{{ node.address }}</span>
|
||||
<span class="loading-manage-dialog__route-address">{{ node.displayAddress || formatAddressText(node.address) }}</span>
|
||||
<span class="loading-manage-dialog__route-tags">
|
||||
<el-tag v-for="tag in node.tags" :key="tag" size="small">{{ tag }}</el-tag>
|
||||
</span>
|
||||
@@ -804,7 +820,7 @@
|
||||
@change="handleCarrierTypeChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="loading-manage-dialog__grid">
|
||||
<div class="loading-manage-dialog__grid loading-manage-dialog__task-grid">
|
||||
<el-form-item v-if="!['自运', '网货平台'].includes(dialogForm.carrierType)" label="承运商" required>
|
||||
<el-select
|
||||
v-model="dialogForm.carrierName"
|
||||
@@ -893,10 +909,24 @@
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div v-if="isStandaloneFormPage" class="loading-manage-dialog__footer">
|
||||
<template v-if="dialogReadonly">
|
||||
<el-button @click="closeLoadingDialog">关闭</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="closeLoadingDialog">关闭</el-button>
|
||||
<el-button type="primary" plain :loading="dialogSaving === 'draft'" @click="saveDraft">
|
||||
暂存
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="dialogSaving === 'submit'" @click="submitLoading">
|
||||
{{ dialogMode === 'reassign' ? '确认派单' : '确认' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
<template v-if="!isStandaloneFormPage" #footer>
|
||||
<div class="loading-manage-dialog__footer">
|
||||
<template v-if="dialogReadonly">
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
<el-button @click="closeLoadingDialog">关闭</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="clearLoadingDialog">清空</el-button>
|
||||
@@ -909,7 +939,7 @@
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</component>
|
||||
<el-dialog v-model="routeChangeVisible" title="变更运输路线" append-to-body destroy-on-close width="78%" class="loading-route-change-dialog">
|
||||
<el-tabs v-model="routeChangeTab">
|
||||
<el-tab-pane label="变更路线" name="change">
|
||||
@@ -1192,19 +1222,51 @@ export default {
|
||||
typeof item === 'string' ? { name: item.split('/').pop(), url: item } : { name: item.name || item.fileName || '附件', url: item.url || item.link || item.fileUrl }
|
||||
);
|
||||
},
|
||||
isStandaloneFormPage() {
|
||||
return (
|
||||
this.$route.path === '/business/loading-manage/form' &&
|
||||
['add', 'edit'].includes(this.$route.query.mode)
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadTransportTypeOptions();
|
||||
this.loadTable();
|
||||
if (this.isStandaloneFormPage) {
|
||||
this.syncStandaloneTagTitle();
|
||||
this.initStandaloneFormPage();
|
||||
}
|
||||
const detailId = this.$route.query.detailId;
|
||||
if (detailId) this.openLoadingDialog('view', { id: detailId });
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
if (this.isStandaloneFormPage) {
|
||||
this.syncStandaloneTagTitle();
|
||||
if (
|
||||
to.query.mode !== from?.query?.mode ||
|
||||
String(to.query.id || '') !== String(from?.query.id || '')
|
||||
) {
|
||||
this.initStandaloneFormPage();
|
||||
}
|
||||
} else if (from?.query?.mode && this.dialogVisible) {
|
||||
this.closeLoadingDialog();
|
||||
}
|
||||
},
|
||||
'$route.query.detailId'(detailId) {
|
||||
if (detailId) this.openLoadingDialog('view', { id: detailId });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
syncStandaloneTagTitle() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
const title = this.$route.query.mode === 'edit' ? '编辑配载管理' : '新增配载管理';
|
||||
if (this.$route.query.name === title) return;
|
||||
this.$store.commit('SET_TAG', {
|
||||
fullPath: this.$route.fullPath,
|
||||
query: { ...this.$route.query, name: title },
|
||||
});
|
||||
},
|
||||
buildQueryParams(form) {
|
||||
const params = { ...form };
|
||||
const [startDateStart, startDateEnd] = params.startDateRange || [];
|
||||
@@ -1333,6 +1395,64 @@ export default {
|
||||
.flatMap(value => this.splitText(value));
|
||||
return [...new Set(values)].join(',') || '-';
|
||||
},
|
||||
formatWaybillAddress(row, type) {
|
||||
const prefix = type === 'departure' ? 'departure' : 'arrival';
|
||||
const address = row[`${prefix}Address`] || row[`${prefix}Name`] || '';
|
||||
const region = this.mergeRouteAddressParts(
|
||||
this.routeProvinceName(row, prefix),
|
||||
row[`${prefix}CityName`] || row[`${prefix}City`],
|
||||
row[`${prefix}DistrictName`] || row[`${prefix}District`]
|
||||
);
|
||||
const source = this.mergeRouteAddressParts(region, '', address);
|
||||
const parts = this.parseWaybillAddress(source);
|
||||
const detailAddress =
|
||||
row[`${prefix}DetailAddress`] ||
|
||||
row[`${prefix}AddressDetail`] ||
|
||||
parts.detailAddress ||
|
||||
address;
|
||||
return [parts.province, parts.city, parts.district, detailAddress]
|
||||
.map(value => String(value || '').trim())
|
||||
.filter(Boolean)
|
||||
.join(' / ') || '-';
|
||||
},
|
||||
parseWaybillAddress(address) {
|
||||
let rest = String(address || '').trim();
|
||||
const result = { province: '', city: '', district: '', detailAddress: '' };
|
||||
if (!rest) return result;
|
||||
|
||||
const municipalityMatch = rest.match(/^(北京市|天津市|上海市|重庆市)/);
|
||||
if (municipalityMatch) {
|
||||
result.province = municipalityMatch[0];
|
||||
result.city = municipalityMatch[0];
|
||||
rest = rest.slice(municipalityMatch[0].length);
|
||||
} else {
|
||||
const provinceMatch = rest.match(/^(.+?(?:省|自治区|特别行政区))/);
|
||||
if (provinceMatch) {
|
||||
result.province = provinceMatch[0];
|
||||
rest = rest.slice(provinceMatch[0].length);
|
||||
}
|
||||
const cityMatch = rest.match(/^(.+?(?:市|自治州|地区|盟))/);
|
||||
if (cityMatch) {
|
||||
result.city = cityMatch[0];
|
||||
rest = rest.slice(cityMatch[0].length);
|
||||
}
|
||||
}
|
||||
|
||||
const districtMatch = rest.match(/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/);
|
||||
if (districtMatch) {
|
||||
result.district = districtMatch[0];
|
||||
rest = rest.slice(districtMatch[0].length);
|
||||
}
|
||||
result.detailAddress = rest;
|
||||
return result;
|
||||
},
|
||||
formatAddressText(address) {
|
||||
const parts = this.parseWaybillAddress(address);
|
||||
return [parts.province, parts.city, parts.district, parts.detailAddress]
|
||||
.map(value => String(value || '').trim())
|
||||
.filter(Boolean)
|
||||
.join(' / ') || String(address || '').trim() || '-';
|
||||
},
|
||||
mergeRouteAddressParts(region, district, address) {
|
||||
const values = [region, district, address].map(value => String(value || '').trim()).filter(Boolean);
|
||||
if (!values.length) return '';
|
||||
@@ -1366,8 +1486,8 @@ export default {
|
||||
if (status === 'draft') {
|
||||
return [
|
||||
{ type: 'edit', label: '编辑' },
|
||||
{ type: 'delete', label: '删除' },
|
||||
{ type: 'copy', label: '复制' },
|
||||
{ type: 'delete', label: '删除' },
|
||||
];
|
||||
}
|
||||
if (status === 'pending') {
|
||||
@@ -1406,7 +1526,24 @@ export default {
|
||||
};
|
||||
actionMap[type]?.();
|
||||
},
|
||||
initStandaloneFormPage() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
this.openLoadingDialog(this.$route.query.mode === 'edit' ? 'edit' : 'add', {
|
||||
id: this.$route.query.id,
|
||||
});
|
||||
},
|
||||
async openLoadingDialog(mode, row) {
|
||||
if (['add', 'edit'].includes(mode) && !this.isStandaloneFormPage) {
|
||||
this.$router.push({
|
||||
path: '/business/loading-manage/form',
|
||||
query: {
|
||||
mode,
|
||||
...(mode === 'edit' && row?.id ? { id: row.id } : {}),
|
||||
name: mode === 'edit' ? '编辑配载管理' : '新增配载管理',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.dialogMode = mode;
|
||||
this.dialogVisible = true;
|
||||
this.dialogLoading = true;
|
||||
@@ -1439,6 +1576,14 @@ export default {
|
||||
this.dialogLoading = false;
|
||||
}
|
||||
},
|
||||
closeLoadingDialog() {
|
||||
if (this.isStandaloneFormPage) {
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push({ path: '/business/loading-manage', query: {} });
|
||||
return;
|
||||
}
|
||||
this.dialogVisible = false;
|
||||
},
|
||||
resetLoadingDialog() {
|
||||
this.resetDialogData();
|
||||
this.dialogSaving = '';
|
||||
@@ -1814,18 +1959,18 @@ export default {
|
||||
},
|
||||
buildRouteNodes(rows) {
|
||||
const nodeMap = new Map();
|
||||
const appendNode = (address, tag) => {
|
||||
const appendNode = (address, displayAddress, tag) => {
|
||||
if (!address) return;
|
||||
if (!nodeMap.has(address)) nodeMap.set(address, { address, tags: [] });
|
||||
if (!nodeMap.has(address)) nodeMap.set(address, { address, displayAddress, tags: [] });
|
||||
nodeMap.get(address).tags.push(tag);
|
||||
};
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const departure = row.departureAddress || row.departureName;
|
||||
appendNode(departure, `装${rowIndex + 1}`);
|
||||
appendNode(departure, this.formatWaybillAddress(row, 'departure'), `装${rowIndex + 1}`);
|
||||
});
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const arrival = row.arrivalAddress || row.arrivalName;
|
||||
appendNode(arrival, `卸${rowIndex + 1}`);
|
||||
appendNode(arrival, this.formatWaybillAddress(row, 'arrival'), `卸${rowIndex + 1}`);
|
||||
});
|
||||
return [...nodeMap.values()].map((node, index) => ({
|
||||
...node,
|
||||
@@ -1850,10 +1995,24 @@ export default {
|
||||
const rows = [...this.routeNodes];
|
||||
const [target] = rows.splice(this.routeDragIndex, 1);
|
||||
rows.splice(index, 0, target);
|
||||
this.routeNodes = rows;
|
||||
this.routeDragIndex = -1;
|
||||
if (!this.validateRouteEndpoints(rows)) return;
|
||||
this.routeNodes = rows;
|
||||
this.syncRouteAddressFields();
|
||||
},
|
||||
validateRouteEndpoints(nodes = this.routeNodes) {
|
||||
const startTags = nodes[0]?.tags || [];
|
||||
if (startTags.some(tag => String(tag).startsWith('卸'))) {
|
||||
ElMessage.warning('起点不能设置卸货');
|
||||
return false;
|
||||
}
|
||||
const endTags = nodes[nodes.length - 1]?.tags || [];
|
||||
if (endTags.some(tag => String(tag).startsWith('装'))) {
|
||||
ElMessage.warning('终点不能设置装货');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
syncRouteAddressFields() {
|
||||
const nodes = this.routeNodes.filter(item => item.address);
|
||||
this.dialogForm.departureAddress = nodes[0]?.address || '';
|
||||
@@ -1945,6 +2104,13 @@ export default {
|
||||
if (draftMode) {
|
||||
return true;
|
||||
}
|
||||
if (this.dialogMode === 'add' && this.selectedWaybillRows.length < 2) {
|
||||
ElMessage.warning('请至少选择两条待配载运单');
|
||||
return false;
|
||||
}
|
||||
if (!this.validateRouteEndpoints()) {
|
||||
return false;
|
||||
}
|
||||
if (!this.dialogForm.departureAddress || !this.dialogForm.arrivalAddress) {
|
||||
ElMessage.warning('请确认配载路线');
|
||||
return false;
|
||||
@@ -1995,7 +2161,7 @@ export default {
|
||||
await loadingApi.submit(payload);
|
||||
ElMessage.success('确认成功');
|
||||
}
|
||||
this.dialogVisible = false;
|
||||
this.closeLoadingDialog();
|
||||
this.loadTable();
|
||||
} finally {
|
||||
this.dialogSaving = '';
|
||||
@@ -2249,11 +2415,6 @@ export default {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.loading-manage-page__vehicle-header {
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.loading-manage-page__status {
|
||||
color: #303133;
|
||||
}
|
||||
@@ -2340,7 +2501,7 @@ export default {
|
||||
|
||||
.loading-manage-dialog__waybill-route-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr);
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
margin-bottom: 16px;
|
||||
@@ -2424,6 +2585,23 @@ export default {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__task-grid {
|
||||
:deep(.el-form-item__label) {
|
||||
flex: 0 0 100px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content),
|
||||
:deep(.el-input),
|
||||
:deep(.el-select),
|
||||
:deep(.el-autocomplete),
|
||||
:deep(.el-date-editor.el-input),
|
||||
:deep(.el-date-editor.el-input__wrapper) {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.dialog-section-title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2503,6 +2681,90 @@ export default {
|
||||
.loading-route-change-dialog__record-content { white-space: normal; overflow-wrap: anywhere; line-height: 22px; }
|
||||
}
|
||||
.loading-common-address-dialog__toolbar { display: flex; gap: 8px; margin-bottom: 12px; }.loading-common-address-dialog__toolbar .el-input { flex: 1; }
|
||||
:global(.loading-manage-form-page) {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
background: #f5f6fa;
|
||||
}
|
||||
:global(.loading-manage-form-page .loading-manage-dialog__body) {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
padding-right: 0;
|
||||
}
|
||||
:global(.loading-manage-form-dialog) {
|
||||
width: 100% !important;
|
||||
min-height: 100%;
|
||||
margin: 0 !important;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
:global(.loading-manage-form-dialog .el-dialog__body) {
|
||||
max-height: none;
|
||||
padding: 20px 24px 96px;
|
||||
background: #f5f6fa;
|
||||
}
|
||||
:global(.el-overlay:has(.loading-manage-form-dialog)) {
|
||||
position: fixed !important;
|
||||
inset: 92px 0 0 230px !important;
|
||||
z-index: 100 !important;
|
||||
overflow: auto;
|
||||
background: transparent;
|
||||
}
|
||||
:global(.el-overlay:has(.loading-manage-form-dialog) .el-overlay-dialog) {
|
||||
min-height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
:global(.loading-manage-form-dialog .el-dialog__header) {
|
||||
padding: 18px 24px;
|
||||
margin-right: 0;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eff1f7;
|
||||
}
|
||||
:global(.loading-manage-form-dialog .el-dialog__footer) {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 230px;
|
||||
z-index: 1001;
|
||||
box-sizing: border-box;
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #eff1f7;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
:global(.loading-manage-form-page .loading-manage-dialog__footer) {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 230px;
|
||||
z-index: 1001;
|
||||
box-sizing: border-box;
|
||||
min-height: 64px;
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #eff1f7;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
:global(.avue--collapse .el-overlay:has(.loading-manage-form-dialog)) {
|
||||
left: 60px !important;
|
||||
}
|
||||
:global(.avue-layout--horizontal .el-overlay:has(.loading-manage-form-dialog)) {
|
||||
top: 92px !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
:global(.avue--collapse .loading-manage-form-dialog .el-dialog__footer) {
|
||||
left: 60px;
|
||||
}
|
||||
:global(.avue-layout--horizontal .loading-manage-form-dialog .el-dialog__footer) {
|
||||
left: 0;
|
||||
}
|
||||
:global(.avue--collapse .loading-manage-form-page .loading-manage-dialog__footer) {
|
||||
left: 60px;
|
||||
}
|
||||
:global(.avue-layout--horizontal .loading-manage-form-page .loading-manage-dialog__footer) {
|
||||
left: 0;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.loading-manage-dialog__grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<basic-container class="project-apply-page">
|
||||
<avue-crud
|
||||
v-if="!isProjectFormPage"
|
||||
:option="tableOption"
|
||||
:table-loading="loading"
|
||||
:data="data"
|
||||
@@ -83,22 +84,23 @@
|
||||
</avue-crud>
|
||||
|
||||
<empty-pagination
|
||||
v-if="!isProjectFormPage"
|
||||
:page="page"
|
||||
@size-change="sizeChange"
|
||||
@current-change="currentChange"
|
||||
@load="onLoad(page, query)"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="projectBox"
|
||||
:title="projectDialogTitle"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
width="1440px"
|
||||
top="4vh"
|
||||
class="project-apply-dialog"
|
||||
<component
|
||||
:is="projectFormContainer"
|
||||
v-if="projectBox"
|
||||
v-bind="projectFormContainerProps"
|
||||
@update:model-value="projectBox = $event"
|
||||
@closed="resetProjectDialog"
|
||||
>
|
||||
<div v-if="isProjectFormPage" class="project-apply-page__title">
|
||||
{{ projectDialogTitle }}
|
||||
</div>
|
||||
<el-form
|
||||
ref="projectForm"
|
||||
:model="form"
|
||||
@@ -215,6 +217,7 @@
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:loading="customerLoading"
|
||||
@visible-change="visible => visible && loadCustomerOptions('客户')"
|
||||
@change="handleCustomerChange"
|
||||
@@ -235,6 +238,7 @@
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:loading="carrierLoading"
|
||||
@visible-change="visible => visible && loadCustomerOptions('承运商')"
|
||||
@change="handleCarrierChange"
|
||||
@@ -602,7 +606,6 @@
|
||||
</template>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div
|
||||
class="project-apply-dialog__footer"
|
||||
:class="{ 'project-apply-dialog__footer--change': isChangeDialog }"
|
||||
@@ -614,10 +617,11 @@
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitChangeProject">
|
||||
提交
|
||||
</el-button>
|
||||
<el-button @click="handleCancelProject">取消</el-button>
|
||||
<el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button>
|
||||
<el-button v-if="isProjectFormPage" @click="closeProjectForm">返回</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="handleCancelProject">取消</el-button>
|
||||
<el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button>
|
||||
<el-button
|
||||
v-if="!dialogReadonly"
|
||||
type="primary"
|
||||
@@ -634,10 +638,10 @@
|
||||
>
|
||||
提交
|
||||
</el-button>
|
||||
<el-button v-if="isProjectFormPage" @click="closeProjectForm">返回</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</component>
|
||||
|
||||
<el-dialog
|
||||
v-model="userBox"
|
||||
@@ -1005,6 +1009,24 @@ export default {
|
||||
isChangeDialog() {
|
||||
return this.dialogType === 'change';
|
||||
},
|
||||
isProjectFormPage() {
|
||||
return this.$route.path === '/business/project-apply/form';
|
||||
},
|
||||
projectFormContainer() {
|
||||
return this.isProjectFormPage ? 'div' : 'el-dialog';
|
||||
},
|
||||
projectFormContainerProps() {
|
||||
if (this.isProjectFormPage) return { class: 'project-apply-page-form' };
|
||||
return {
|
||||
modelValue: this.projectBox,
|
||||
title: this.projectDialogTitle,
|
||||
appendToBody: true,
|
||||
destroyOnClose: true,
|
||||
width: '1440px',
|
||||
top: '4vh',
|
||||
class: 'project-apply-dialog',
|
||||
};
|
||||
},
|
||||
isBasicInfoReadonly() {
|
||||
return this.dialogReadonly || this.isChangeDialog;
|
||||
},
|
||||
@@ -1037,6 +1059,9 @@ export default {
|
||||
this.loadDeptOptions();
|
||||
this.loadCargoTypeOptions();
|
||||
this.loadTransportTypeOptions();
|
||||
if (this.isProjectFormPage) {
|
||||
this.openProjectFormPage();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildTableOption() {
|
||||
@@ -1055,6 +1080,26 @@ export default {
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
|
||||
},
|
||||
openProjectFormPage() {
|
||||
const type = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||
const id = this.$route.query.id;
|
||||
this.dialogType = type;
|
||||
this.dialogReadonly = false;
|
||||
this.projectBox = true;
|
||||
if (type === 'add') {
|
||||
this.applyProjectDetail({ ...emptyForm(), projectType: '普通项目' });
|
||||
this.fillDefaultUsers();
|
||||
return;
|
||||
}
|
||||
this.api.getDetail(id).then(res => this.applyProjectDetail(res.data.data || {}));
|
||||
},
|
||||
closeProjectForm() {
|
||||
if (this.isProjectFormPage) {
|
||||
this.$router.push('/business/project-apply');
|
||||
} else {
|
||||
this.projectBox = false;
|
||||
}
|
||||
},
|
||||
statusValue(row) {
|
||||
return row[this.config.statusProp || 'status'];
|
||||
},
|
||||
@@ -1265,6 +1310,16 @@ export default {
|
||||
});
|
||||
},
|
||||
openProjectDialog(type, row = {}) {
|
||||
if (['add', 'edit'].includes(type)) {
|
||||
this.$router.push({
|
||||
path: '/business/project-apply/form',
|
||||
query:
|
||||
type === 'edit'
|
||||
? { mode: type, id: row.id, name: `编辑${this.config.title}` }
|
||||
: { mode: type, name: `新增${this.config.title}` },
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.dialogType = type;
|
||||
this.dialogReadonly = type === 'view';
|
||||
this.projectBox = true;
|
||||
@@ -1390,7 +1445,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.$message.success('操作成功!');
|
||||
this.projectBox = false;
|
||||
this.closeProjectForm();
|
||||
this.onLoad(this.page, this.query);
|
||||
},
|
||||
error => {
|
||||
@@ -1412,7 +1467,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.$message.success('保存成功!');
|
||||
this.projectBox = false;
|
||||
this.closeProjectForm();
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -1420,8 +1475,12 @@ export default {
|
||||
});
|
||||
},
|
||||
handleCancelProject() {
|
||||
if (this.isProjectFormPage) {
|
||||
this.closeProjectForm();
|
||||
return;
|
||||
}
|
||||
if (!['add', 'majorSupplement', 'change'].includes(this.dialogType)) {
|
||||
this.projectBox = false;
|
||||
this.closeProjectForm();
|
||||
return;
|
||||
}
|
||||
this.$confirm('确认后直接关闭弹窗,列表无数据变更,是否继续?', '提示', {
|
||||
@@ -1429,7 +1488,7 @@ export default {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
this.projectBox = false;
|
||||
this.closeProjectForm();
|
||||
});
|
||||
},
|
||||
saveChangeProject() {
|
||||
@@ -1456,7 +1515,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.$message.success(`${needSubmit ? '提交' : '保存'}成功!`);
|
||||
this.projectBox = false;
|
||||
this.closeProjectForm();
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -1905,8 +1964,8 @@ export default {
|
||||
|
||||
&__grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
column-gap: 72px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
column-gap: 24px;
|
||||
row-gap: 0;
|
||||
margin-bottom: 12px;
|
||||
background: #fff;
|
||||
@@ -2042,17 +2101,62 @@ export default {
|
||||
background: #f5f6fa;
|
||||
}
|
||||
|
||||
.project-apply-page-form {
|
||||
min-height: 100%;
|
||||
padding: 20px 24px 92px;
|
||||
background: #f5f6fa;
|
||||
}
|
||||
|
||||
.project-apply-page__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
color: #303133;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
|
||||
&::before {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
background: #409eff;
|
||||
border-radius: 2px;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.project-apply-dialog__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
|
||||
.project-apply-page-form & {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
left: 230px;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
margin: 0;
|
||||
padding: 12px 24px;
|
||||
border-top: 1px solid #eff1f7;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
&--change {
|
||||
justify-content: flex-start;
|
||||
padding-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.avue--collapse .project-apply-page-form .project-apply-dialog__footer) {
|
||||
left: 60px;
|
||||
}
|
||||
|
||||
:global(.avue-layout--horizontal .project-apply-page-form .project-apply-dialog__footer) {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.project-apply-user-dialog {
|
||||
&__search {
|
||||
display: grid;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="270" />
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="270" standalone-form-page />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :detail-id="$route.query.detailId" :menu-width="220" />
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :detail-id="$route.query.detailId" :menu-width="220" standalone-form-page />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -92,11 +92,7 @@
|
||||
label="已关联运单"
|
||||
width="115"
|
||||
/><el-table-column prop="unRelatedWaybillCount" label="未关联运单" width="115" />
|
||||
<el-table-column prop="status" label="状态" width="100"
|
||||
><template #default="{ row }">{{
|
||||
Number(row.status) === 1 ? '启用' : '停用'
|
||||
}}</template></el-table-column
|
||||
><el-table-column prop="createTime" label="创建时间" min-width="170" />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" />
|
||||
<el-table-column
|
||||
prop="auditStatus"
|
||||
label="审核状态"
|
||||
@@ -146,6 +142,65 @@
|
||||
</section>
|
||||
</basic-container>
|
||||
|
||||
<el-dialog v-model="detailVisible" width="960px" destroy-on-close>
|
||||
<template #header>
|
||||
<div class="voucher-manage-page__dialog-title">凭证详情</div>
|
||||
</template>
|
||||
<el-descriptions v-loading="detailLoading" :column="2" border>
|
||||
<el-descriptions-item label="项目名称">
|
||||
{{ detailValue(detailRow.projectName) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="凭证批次号">
|
||||
{{ detailValue(detailRow.voucherBatchNo) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="运单批次号" :span="2">
|
||||
{{ detailValue(detailRow.waybillBatchNo) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文件名称" :span="2">
|
||||
<el-link v-if="detailRow.fileUrl" type="primary" @click="download(detailRow)">
|
||||
{{ detailValue(detailRow.fileName) }}
|
||||
</el-link>
|
||||
<template v-else>{{ detailValue(detailRow.fileName) }}</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上传来源">
|
||||
{{ detailValue(detailRow.uploadSource) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="承运商名称">
|
||||
{{ detailValue(detailRow.carrierName) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处理状态">
|
||||
{{ detailValue(detailRow.processStatus) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="审核状态">
|
||||
{{ detailValue(detailRow.auditStatus) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="凭证数量">
|
||||
{{ detailValue(detailRow.voucherCount) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="已关联运单">
|
||||
{{ detailValue(detailRow.relatedWaybillCount) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="未关联运单">
|
||||
{{ detailValue(detailRow.unRelatedWaybillCount) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人">
|
||||
{{ detailValue(detailRow.createUserName) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ detailValue(detailRow.createTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新人">
|
||||
{{ detailValue(detailRow.updateUserName) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ detailValue(detailRow.updateTime) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="uploadVisible"
|
||||
:title="editing.id ? '更换运单批次' : '批量导入凭证'"
|
||||
@@ -414,6 +469,9 @@ const loading = ref(false),
|
||||
rows = ref([]),
|
||||
createRange = ref([]),
|
||||
searchExpanded = ref(false),
|
||||
detailVisible = ref(false),
|
||||
detailLoading = ref(false),
|
||||
detailRow = ref({}),
|
||||
uploadVisible = ref(false),
|
||||
batchVisible = ref(false),
|
||||
progressVisible = ref(false),
|
||||
@@ -875,7 +933,21 @@ const removeRow = row =>
|
||||
ElMessage.success('删除成功');
|
||||
load();
|
||||
});
|
||||
const view = row => ElMessage.info(`凭证批次:${row.voucherBatchNo}`);
|
||||
const detailValue = value =>
|
||||
value === undefined || value === null || value === '' ? '-' : value;
|
||||
const view = async row => {
|
||||
detailVisible.value = true;
|
||||
detailLoading.value = true;
|
||||
detailRow.value = { ...row };
|
||||
try {
|
||||
const res = await api.getDetail(row.id);
|
||||
detailRow.value = res.data?.data || row;
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '获取凭证详情失败');
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
};
|
||||
const download = row => window.open(row.fileUrl, '_blank');
|
||||
load();
|
||||
</script>
|
||||
@@ -914,6 +986,21 @@ load();
|
||||
padding: 0;
|
||||
min-height: auto;
|
||||
}
|
||||
&__dialog-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #303133;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
|
||||
&::before {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
background: #409eff;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -957,8 +1044,9 @@ load();
|
||||
}
|
||||
&__batch-search {
|
||||
padding: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
@@ -976,6 +1064,11 @@ load();
|
||||
:deep(.voucher-manage-page__project-select) {
|
||||
width: 280px;
|
||||
}
|
||||
:deep(.voucher-manage-page__batch-search .el-input),
|
||||
:deep(.voucher-manage-page__batch-search .el-input-number),
|
||||
:deep(.voucher-manage-page__batch-search .el-date-editor) {
|
||||
width: 50%;
|
||||
}
|
||||
:deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
--el-table-row-hover-bg-color: #f5f7fa;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :detail-id="$route.query.detailId" :menu-width="250" />
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :detail-id="$route.query.detailId" :menu-width="250" standalone-form-page />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -0,0 +1,764 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" width="96%" top="2vh" append-to-body destroy-on-close>
|
||||
<div v-loading="loading" class="formal-editor">
|
||||
<section class="formal-editor__section">
|
||||
<div class="dialog-section-title">结算基本信息</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-row :gutter="24">
|
||||
<el-col v-for="field in fields" :key="field.prop" :span="8">
|
||||
<el-form-item :label="field.label" :prop="field.prop">
|
||||
<el-date-picker
|
||||
v-if="field.type === 'date' && editable"
|
||||
v-model="form[field.prop]"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'contract' && editable"
|
||||
v-model="form.contractId"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="loadContracts"
|
||||
placeholder="请选择合同"
|
||||
@change="handleContractChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contracts"
|
||||
:key="item.id"
|
||||
:label="`${item.contractNo} / ${item.contractName}`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input-number
|
||||
v-else-if="field.type === 'number' && editable"
|
||||
v-model="form[field.prop]"
|
||||
:min="0.000001"
|
||||
:precision="6"
|
||||
:controls="false"
|
||||
/>
|
||||
<span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span>
|
||||
<span v-else-if="field.type === 'contract'">{{
|
||||
displayValue(form.contractName)
|
||||
}}</span>
|
||||
<span v-else>{{ displayValue(form[field.prop]) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-if="editable"
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
<span v-else>{{ displayValue(form.remark) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="formal-editor__section">
|
||||
<div class="formal-editor__section-head">
|
||||
<div class="dialog-section-title">来源预结算单</div>
|
||||
<div class="formal-editor__actions">
|
||||
<el-button v-if="editable" type="primary" plain @click="openCandidateDialog"
|
||||
>选择预结算单</el-button
|
||||
>
|
||||
<el-button v-if="editable" type="primary" plain @click="openDetailDialog"
|
||||
>选择结算明细</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="sources" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in sourceTableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="editable" label="操作" width="90" align="center">
|
||||
<template #default="{ $index }"
|
||||
><el-link type="danger" @click="removeSource($index)">删除</el-link></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section v-if="details.length" class="formal-editor__section">
|
||||
<div class="dialog-section-title">结算明细</div>
|
||||
<el-table :data="details" border>
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in detailTableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="editable" label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<div class="formal-editor__links">
|
||||
<el-link v-if="row.formalSettlementId" type="primary" @click="openAdjustDialog(row)"
|
||||
>调整</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="!row.sourcePreSettlementId"
|
||||
type="danger"
|
||||
@click="removeDetail($index)"
|
||||
>删除</el-link
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section class="formal-editor__section">
|
||||
<div class="dialog-section-title">附件</div>
|
||||
<vehicle-attachment-upload
|
||||
v-model="attachments"
|
||||
:readonly="!editable"
|
||||
:multiple="true"
|
||||
:limit="20"
|
||||
:max-size="500"
|
||||
:file-types="attachmentFileTypes"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section v-if="readonly && payments.length" class="formal-editor__section">
|
||||
<div class="dialog-section-title">付款信息</div>
|
||||
<el-table :data="payments" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in paymentTableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else-if="column.prop === 'paymentTypeName'">{{
|
||||
row.paymentType === 'final' ? '尾款' : displayValue(row.paymentType)
|
||||
}}</span>
|
||||
<span v-else-if="column.prop === 'billStatusName'">{{
|
||||
row.billStatus === 'reviewing' ? '审批中' : displayValue(row.billStatus)
|
||||
}}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button v-if="editable" type="primary" :loading="saving" @click="handleSave"
|
||||
>提交</el-button
|
||||
>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="candidate.visible" title="选择预结算单" width="88%" append-to-body>
|
||||
<el-form :model="candidate.query" inline label-position="right" label-width="160px">
|
||||
<el-form-item label="预结算单号"
|
||||
><el-input v-model="candidate.query.preSettlementNo" clearable
|
||||
/></el-form-item>
|
||||
<el-form-item label="合同编号"
|
||||
><el-input v-model="candidate.query.contractNo" clearable
|
||||
/></el-form-item>
|
||||
<el-form-item
|
||||
><el-button @click="resetCandidates">重置</el-button
|
||||
><el-button type="primary" @click="loadCandidates">查询</el-button></el-form-item
|
||||
>
|
||||
</el-form>
|
||||
<el-table
|
||||
ref="candidateTable"
|
||||
v-loading="candidate.loading"
|
||||
:data="candidate.rows"
|
||||
border
|
||||
@selection-change="candidate.selected = $event"
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in candidateTableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }"
|
||||
><span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span
|
||||
><span v-else>{{ displayValue(row[column.prop]) }}</span></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="formal-editor__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="candidate.page.current"
|
||||
v-model:page-size="candidate.page.size"
|
||||
:total="candidate.page.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadCandidates"
|
||||
@size-change="loadCandidates"
|
||||
/>
|
||||
</div>
|
||||
<template #footer
|
||||
><el-button @click="candidate.visible = false">取消</el-button
|
||||
><el-button type="primary" @click="confirmCandidates">确定</el-button></template
|
||||
>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="detailCandidate.visible" title="选择结算明细" width="92%" append-to-body>
|
||||
<el-form :model="detailCandidate.query" inline label-position="right" label-width="160px">
|
||||
<el-form-item label="批次号"
|
||||
><el-input v-model="detailCandidate.query.batchNo" clearable
|
||||
/></el-form-item>
|
||||
<el-form-item label="费用日期"
|
||||
><el-date-picker
|
||||
v-model="detailCandidate.query.feeDateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/></el-form-item>
|
||||
<el-form-item
|
||||
><el-button @click="resetDetailCandidates">重置</el-button
|
||||
><el-button type="primary" @click="loadDetailCandidates">查询</el-button></el-form-item
|
||||
>
|
||||
</el-form>
|
||||
<el-table
|
||||
v-loading="detailCandidate.loading"
|
||||
:data="detailCandidate.rows"
|
||||
border
|
||||
@selection-change="detailCandidate.selected = $event"
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in candidateDetailTableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }"
|
||||
><span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span
|
||||
><span v-else>{{ displayValue(row[column.prop]) }}</span></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="formal-editor__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="detailCandidate.page.current"
|
||||
v-model:page-size="detailCandidate.page.size"
|
||||
:total="detailCandidate.page.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadDetailCandidates"
|
||||
@size-change="loadDetailCandidates"
|
||||
/>
|
||||
</div>
|
||||
<template #footer
|
||||
><el-button @click="detailCandidate.visible = false">取消</el-button
|
||||
><el-button type="primary" @click="confirmDetailCandidates">确定</el-button></template
|
||||
>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="adjust.visible" title="结算明细调整" width="92%" append-to-body>
|
||||
<el-table v-loading="adjust.loading" :data="adjust.rows" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column prop="cargoName" label="货物名称" min-width="130" align="center" />
|
||||
<el-table-column prop="cargoType" label="货物类型" min-width="130" align="center" />
|
||||
<el-table-column label="运输总量" min-width="130" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.transportQuantity"
|
||||
:min="0"
|
||||
:precision="6"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="里程(KM)" min-width="130" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.mileage"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="运输单价" min-width="130" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.unitPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="运费" min-width="130" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.freightAmount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="结算金额(含税)" min-width="165" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.settlementAmountTax"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="结算金额(不含税)" min-width="180" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.settlementAmountNoTax"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column label="备注" min-width="180" align="center"
|
||||
><template #default="{ row }"><el-input v-model="row.remark" maxlength="200" /></template
|
||||
></el-table-column>
|
||||
</el-table>
|
||||
<el-form label-position="right" label-width="auto" class="formal-editor__adjust-reason">
|
||||
<el-form-item label="调整原因" required
|
||||
><el-input v-model="adjust.reason" type="textarea" maxlength="200" show-word-limit
|
||||
/></el-form-item>
|
||||
</el-form>
|
||||
<template #footer
|
||||
><el-button @click="adjust.visible = false">取消</el-button
|
||||
><el-button type="primary" :loading="adjust.saving" @click="saveAdjustment"
|
||||
>保存</el-button
|
||||
></template
|
||||
>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
adjustDetail,
|
||||
getCandidateDetails,
|
||||
getCandidates,
|
||||
getContractOptions,
|
||||
getDetail,
|
||||
getDetailFees,
|
||||
save,
|
||||
} from '@/api/settlement/formalSettlement';
|
||||
import {
|
||||
createFormalSettlementForm,
|
||||
formalSettlementFormFields,
|
||||
} from '@/option/settlement/formalSettlementForm';
|
||||
import {
|
||||
candidateColumns,
|
||||
candidateDetailColumns,
|
||||
detailColumns,
|
||||
paymentColumns,
|
||||
sourceColumns,
|
||||
} from '@/option/settlement/formalSettlementTable';
|
||||
|
||||
export default {
|
||||
name: 'FormalSettlementEditor',
|
||||
props: { modelValue: Boolean, recordId: [String, Number], readonly: Boolean },
|
||||
emits: ['update:modelValue', 'success'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
saving: false,
|
||||
form: createFormalSettlementForm(),
|
||||
sources: [],
|
||||
details: [],
|
||||
payments: [],
|
||||
attachments: [],
|
||||
attachmentFileTypes: [
|
||||
'pdf',
|
||||
'bmp',
|
||||
'jpeg',
|
||||
'png',
|
||||
'jpg',
|
||||
'doc',
|
||||
'docx',
|
||||
'ppt',
|
||||
'pptx',
|
||||
'xlsx',
|
||||
'xls',
|
||||
'eml',
|
||||
'msg',
|
||||
'zip',
|
||||
'rar',
|
||||
],
|
||||
contracts: [],
|
||||
fields: formalSettlementFormFields,
|
||||
sourceTableColumns: sourceColumns,
|
||||
detailTableColumns: detailColumns,
|
||||
paymentTableColumns: paymentColumns,
|
||||
candidateTableColumns: candidateColumns,
|
||||
candidateDetailTableColumns: candidateDetailColumns,
|
||||
rules: {
|
||||
contractId: [{ required: true, message: '请选择合同', trigger: 'change' }],
|
||||
exchangeRateDate: [{ required: true, message: '请选择汇率日期', trigger: 'change' }],
|
||||
exchangeRate: [{ required: true, message: '请输入结算汇率', trigger: 'blur' }],
|
||||
},
|
||||
candidate: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
query: {},
|
||||
rows: [],
|
||||
selected: [],
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
},
|
||||
detailCandidate: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
query: {},
|
||||
rows: [],
|
||||
selected: [],
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
},
|
||||
adjust: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
saving: false,
|
||||
detailId: null,
|
||||
reason: '',
|
||||
rows: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visible: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
},
|
||||
editable() {
|
||||
return !this.readonly;
|
||||
},
|
||||
title() {
|
||||
return this.readonly ? '查看正式结算单' : this.recordId ? '编辑正式结算单' : '新增正式结算单';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
if (value) this.initialize();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.form = createFormalSettlementForm();
|
||||
this.sources = [];
|
||||
this.details = [];
|
||||
this.payments = [];
|
||||
this.attachments = [];
|
||||
await this.loadContracts();
|
||||
if (!this.recordId) {
|
||||
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await getDetail(this.recordId);
|
||||
const data = this.unwrapData(response);
|
||||
this.form = {
|
||||
...createFormalSettlementForm(),
|
||||
...data,
|
||||
sourcePreSettlementIds: (data.sources || []).map(item => item.preSettlementId),
|
||||
sourceDetailIds: (data.details || [])
|
||||
.filter(item => !item.sourcePreSettlementId)
|
||||
.map(item => item.sourceDetailId),
|
||||
};
|
||||
this.sources = data.sources || [];
|
||||
this.details = data.details || [];
|
||||
this.payments = data.payments || [];
|
||||
this.attachments = this.parseAttachments(data.attachmentsJson);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadContracts(keyword = '') {
|
||||
const response = await getContractOptions(keyword);
|
||||
this.contracts = this.unwrapData(response) || [];
|
||||
},
|
||||
handleContractChange(id) {
|
||||
const contract = this.contracts.find(item => String(item.id) === String(id));
|
||||
if (!contract) return;
|
||||
Object.assign(this.form, contract, {
|
||||
contractId: contract.id,
|
||||
settlementTypeName: contract.settlementType === 'receivable' ? '应收' : '应付',
|
||||
});
|
||||
this.sources = [];
|
||||
this.details = [];
|
||||
this.form.sourcePreSettlementIds = [];
|
||||
this.form.sourceDetailIds = [];
|
||||
},
|
||||
openCandidateDialog() {
|
||||
this.candidate.visible = true;
|
||||
this.candidate.page.current = 1;
|
||||
this.loadCandidates();
|
||||
},
|
||||
async loadCandidates() {
|
||||
this.candidate.loading = true;
|
||||
try {
|
||||
const response = await getCandidates(
|
||||
this.candidate.page.current,
|
||||
this.candidate.page.size,
|
||||
{
|
||||
...this.candidate.query,
|
||||
contractId: this.form.contractId || undefined,
|
||||
}
|
||||
);
|
||||
const data = this.unwrapData(response);
|
||||
this.candidate.rows = data.records || [];
|
||||
this.candidate.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.candidate.loading = false;
|
||||
}
|
||||
},
|
||||
resetCandidates() {
|
||||
this.candidate.query = {};
|
||||
this.candidate.page.current = 1;
|
||||
this.loadCandidates();
|
||||
},
|
||||
confirmCandidates() {
|
||||
const merged = [...this.sources, ...this.candidate.selected];
|
||||
const map = new Map(
|
||||
merged.map(item => [
|
||||
String(item.preSettlementId || item.id),
|
||||
{ ...item, preSettlementId: item.preSettlementId || item.id },
|
||||
])
|
||||
);
|
||||
this.sources = [...map.values()];
|
||||
this.form.sourcePreSettlementIds = this.sources.map(item => item.preSettlementId);
|
||||
if (this.sources.length) {
|
||||
Object.assign(this.form, this.sources[0], {
|
||||
contractId: this.sources[0].contractId,
|
||||
formalSettlementNo: this.form.formalSettlementNo,
|
||||
});
|
||||
}
|
||||
this.form.settlementAmount = this.sources.reduce(
|
||||
(sum, item) => sum + Number(item.settlementAmount || 0),
|
||||
0
|
||||
);
|
||||
this.form.localSettlementAmount =
|
||||
this.form.settlementAmount * Number(this.form.exchangeRate || 1);
|
||||
this.candidate.visible = false;
|
||||
},
|
||||
removeSource(index) {
|
||||
this.sources.splice(index, 1);
|
||||
this.form.sourcePreSettlementIds = this.sources.map(item => item.preSettlementId || item.id);
|
||||
},
|
||||
openDetailDialog() {
|
||||
if (!this.form.contractId) return this.$message.warning('请先选择合同');
|
||||
this.detailCandidate.visible = true;
|
||||
this.detailCandidate.page.current = 1;
|
||||
this.loadDetailCandidates();
|
||||
},
|
||||
async loadDetailCandidates() {
|
||||
this.detailCandidate.loading = true;
|
||||
try {
|
||||
const range = this.detailCandidate.query.feeDateRange || [];
|
||||
const params = {
|
||||
contractId: this.form.contractId,
|
||||
settlementType: this.form.settlementType,
|
||||
batchNo: this.detailCandidate.query.batchNo,
|
||||
feeStartDate: range[0],
|
||||
feeEndDate: range[1],
|
||||
};
|
||||
const response = await getCandidateDetails(
|
||||
this.detailCandidate.page.current,
|
||||
this.detailCandidate.page.size,
|
||||
params
|
||||
);
|
||||
const data = this.unwrapData(response);
|
||||
this.detailCandidate.rows = data.records || [];
|
||||
this.detailCandidate.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.detailCandidate.loading = false;
|
||||
}
|
||||
},
|
||||
resetDetailCandidates() {
|
||||
this.detailCandidate.query = {};
|
||||
this.detailCandidate.page.current = 1;
|
||||
this.loadDetailCandidates();
|
||||
},
|
||||
confirmDetailCandidates() {
|
||||
const existing = new Map(
|
||||
this.details
|
||||
.filter(item => !item.formalSettlementId)
|
||||
.map(item => [String(item.sourceDetailId || item.id), item])
|
||||
);
|
||||
this.detailCandidate.selected.forEach(item =>
|
||||
existing.set(String(item.id), {
|
||||
...item,
|
||||
sourceDetailId: item.id,
|
||||
settlementAmountTax: item.totalAmount,
|
||||
})
|
||||
);
|
||||
this.details = [
|
||||
...this.details.filter(item => item.formalSettlementId),
|
||||
...existing.values(),
|
||||
];
|
||||
this.form.sourceDetailIds = [...existing.values()].map(item => item.sourceDetailId);
|
||||
this.form.settlementAmount =
|
||||
this.sources.reduce((sum, item) => sum + Number(item.settlementAmount || 0), 0) +
|
||||
[...existing.values()].reduce((sum, item) => sum + Number(item.totalAmount || 0), 0);
|
||||
this.detailCandidate.visible = false;
|
||||
},
|
||||
removeDetail(index) {
|
||||
this.details.splice(index, 1);
|
||||
this.form.sourceDetailIds = this.details
|
||||
.filter(item => !item.sourcePreSettlementId)
|
||||
.map(item => item.sourceDetailId);
|
||||
},
|
||||
async openAdjustDialog(row) {
|
||||
this.adjust = {
|
||||
visible: true,
|
||||
loading: true,
|
||||
saving: false,
|
||||
detailId: row.id,
|
||||
reason: '',
|
||||
rows: [],
|
||||
};
|
||||
try {
|
||||
const response = await getDetailFees(row.id);
|
||||
const data = this.unwrapData(response);
|
||||
this.adjust.rows = (data || []).map(item => ({
|
||||
...item,
|
||||
feeItems: this.parseFeeItems(item.feeItemsJson),
|
||||
}));
|
||||
} finally {
|
||||
this.adjust.loading = false;
|
||||
}
|
||||
},
|
||||
async saveAdjustment() {
|
||||
if (!this.adjust.reason.trim()) return this.$message.warning('请输入调整原因');
|
||||
this.adjust.saving = true;
|
||||
try {
|
||||
await adjustDetail({
|
||||
detailId: this.adjust.detailId,
|
||||
changeReason: this.adjust.reason,
|
||||
rows: this.adjust.rows,
|
||||
});
|
||||
this.$message.success('调整保存成功');
|
||||
this.adjust.visible = false;
|
||||
await this.initialize();
|
||||
} finally {
|
||||
this.adjust.saving = false;
|
||||
}
|
||||
},
|
||||
async handleSave() {
|
||||
await this.$refs.formRef.validate();
|
||||
if (!this.form.sourcePreSettlementIds.length && !this.form.sourceDetailIds.length) {
|
||||
return this.$message.warning('请选择预结算单或结算明细');
|
||||
}
|
||||
this.saving = true;
|
||||
try {
|
||||
await save({
|
||||
id: this.form.id,
|
||||
contractId: this.form.contractId,
|
||||
settlementType: this.form.settlementType,
|
||||
sourcePreSettlementIds: this.form.sourcePreSettlementIds,
|
||||
sourceDetailIds: this.form.sourceDetailIds,
|
||||
exchangeRateDate: this.form.exchangeRateDate,
|
||||
exchangeRate: this.form.exchangeRate,
|
||||
attachmentsJson: JSON.stringify(this.attachments || []),
|
||||
remark: this.form.remark,
|
||||
});
|
||||
this.$message.success('保存成功');
|
||||
this.visible = false;
|
||||
this.$emit('success');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
unwrapData(response) {
|
||||
const body = response?.data || response || {};
|
||||
return body?.data || body;
|
||||
},
|
||||
formatMoney(value) {
|
||||
return `${Number(value || 0).toFixed(2)} RMB`;
|
||||
},
|
||||
parseFeeItems(value) {
|
||||
if (!value) return {};
|
||||
if (typeof value === 'object') return value;
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
parseAttachments(value) {
|
||||
if (!value) return [];
|
||||
if (Array.isArray(value)) return value;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.formal-editor__section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.formal-editor__section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.formal-editor__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-editor__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-editor :deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.formal-editor :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
.formal-editor :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||||
background: #fafafa;
|
||||
}
|
||||
.formal-editor__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.formal-editor__adjust-reason {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,396 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" width="94%" top="3vh" append-to-body destroy-on-close>
|
||||
<div v-loading="loading" class="settlement-adjustment-editor">
|
||||
<section class="settlement-adjustment-editor__section">
|
||||
<div class="dialog-section-title">基本信息</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-row :gutter="24">
|
||||
<el-col v-for="field in fields" :key="field.prop" :span="8"
|
||||
><el-form-item :label="field.label" :prop="field.prop"
|
||||
><el-select
|
||||
v-if="field.prop === 'formalSettlementId' && editable"
|
||||
v-model="form.formalSettlementId"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="loadFormalSettlements"
|
||||
placeholder="请选择正式结算单"
|
||||
@change="handleFormalChange"
|
||||
><el-option
|
||||
v-for="item in candidates"
|
||||
:key="item.id"
|
||||
:label="`${item.formalSettlementNo} / ${item.projectName || ''}`"
|
||||
:value="item.id" /></el-select
|
||||
><el-input
|
||||
v-else-if="field.prop === 'adjustmentNo'"
|
||||
v-model="form[field.prop]"
|
||||
disabled
|
||||
/><el-input-number
|
||||
v-else-if="field.money && editable"
|
||||
v-model="form[field.prop]"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
disabled
|
||||
/><span v-else>{{
|
||||
displayValue(
|
||||
field.prop === 'settlementTypeName' ? settlementTypeName : form[field.prop]
|
||||
)
|
||||
}}</span></el-form-item
|
||||
></el-col
|
||||
>
|
||||
<el-col :span="24"
|
||||
><el-form-item label="备注" prop="remark"
|
||||
><el-input
|
||||
v-if="editable"
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/><span v-else>{{ displayValue(form.remark) }}</span></el-form-item
|
||||
></el-col
|
||||
>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="settlement-adjustment-editor__section">
|
||||
<div class="settlement-adjustment-editor__section-head">
|
||||
<div class="dialog-section-title">调整费用</div>
|
||||
<el-button
|
||||
v-if="editable && form.formalSettlementId"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openFeeDialog"
|
||||
>添加费用</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table :data="details" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" /><el-table-column
|
||||
prop="feeType"
|
||||
label="费用类型"
|
||||
min-width="150"
|
||||
align="center"
|
||||
/><el-table-column
|
||||
prop="feeItem"
|
||||
label="费用项目"
|
||||
min-width="180"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/><el-table-column
|
||||
prop="originalAmountTax"
|
||||
label="原金额(含税)"
|
||||
min-width="145"
|
||||
align="center"
|
||||
><template #default="{ row }">{{
|
||||
formatMoney(row.originalAmountTax)
|
||||
}}</template></el-table-column
|
||||
>
|
||||
<el-table-column label="调整金额(含税)" min-width="160" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-if="editable"
|
||||
v-model="row.adjustmentAmountTax"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
@change="recalculate"
|
||||
/><span v-else :class="{ negative: Number(row.adjustmentAmountTax) < 0 }">{{
|
||||
formatMoney(row.adjustmentAmountTax)
|
||||
}}</span></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column label="调整金额(不含税)" min-width="170" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-if="editable"
|
||||
v-model="row.adjustmentAmountNoTax"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
/><span v-else>{{ formatMoney(row.adjustmentAmountNoTax) }}</span></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column label="备注" min-width="180" align="center"
|
||||
><template #default="{ row }"
|
||||
><el-input v-if="editable" v-model="row.remark" maxlength="200" /><span v-else>{{
|
||||
displayValue(row.remark)
|
||||
}}</span></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column v-if="editable" label="操作" width="90" align="center"
|
||||
><template #default="{ $index }"
|
||||
><el-link
|
||||
type="danger"
|
||||
@click="
|
||||
details.splice($index, 1);
|
||||
recalculate();
|
||||
"
|
||||
>删除</el-link
|
||||
></template
|
||||
></el-table-column
|
||||
>
|
||||
<template #empty><el-empty description="请选择调整费用明细" /></template>
|
||||
</el-table>
|
||||
<div class="settlement-adjustment-editor__summary">
|
||||
调整金额合计:<strong :class="{ negative: Number(form.adjustmentAmount) < 0 }">{{
|
||||
formatMoney(form.adjustmentAmount)
|
||||
}}</strong
|
||||
>;调整后结算金额:<strong>{{ formatMoney(form.adjustedSettlementAmount) }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<template #footer
|
||||
><el-button @click="visible = false">取消</el-button
|
||||
><el-button v-if="editable" type="primary" :loading="saving" @click="handleSave"
|
||||
>保存</el-button
|
||||
></template
|
||||
>
|
||||
|
||||
<el-dialog v-model="feeDialog.visible" title="选择调整费用" width="88%" append-to-body>
|
||||
<el-table
|
||||
ref="feeTable"
|
||||
:data="feeRows"
|
||||
border
|
||||
@selection-change="feeDialog.selected = $event"
|
||||
><el-table-column type="selection" width="52" align="center" /><el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="64"
|
||||
align="center"
|
||||
/><el-table-column
|
||||
prop="documentNo"
|
||||
label="单据号"
|
||||
min-width="150"
|
||||
align="center"
|
||||
/><el-table-column
|
||||
prop="feeType"
|
||||
label="费用类型"
|
||||
min-width="140"
|
||||
align="center"
|
||||
/><el-table-column
|
||||
prop="feeItem"
|
||||
label="费用项目"
|
||||
min-width="180"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/><el-table-column
|
||||
prop="originalAmountTax"
|
||||
label="原金额(含税)"
|
||||
min-width="145"
|
||||
align="center"
|
||||
><template #default="{ row }">{{
|
||||
formatMoney(row.originalAmountTax)
|
||||
}}</template></el-table-column
|
||||
></el-table
|
||||
>
|
||||
<template #footer
|
||||
><el-button @click="feeDialog.visible = false">取消</el-button
|
||||
><el-button type="primary" @click="confirmFees">确定</el-button></template
|
||||
>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/settlement/settlementAdjustment';
|
||||
import {
|
||||
createSettlementAdjustmentForm,
|
||||
settlementAdjustmentFormFields,
|
||||
} from '@/option/settlement/settlementAdjustmentForm';
|
||||
export default {
|
||||
name: 'SettlementAdjustmentEditor',
|
||||
props: { modelValue: Boolean, recordId: [String, Number], readonly: Boolean },
|
||||
emits: ['update:modelValue', 'success'],
|
||||
data: () => ({
|
||||
loading: false,
|
||||
saving: false,
|
||||
form: createSettlementAdjustmentForm(),
|
||||
fields: settlementAdjustmentFormFields,
|
||||
details: [],
|
||||
candidates: [],
|
||||
feeRows: [],
|
||||
rules: {
|
||||
formalSettlementId: [{ required: true, message: '请选择关联正式结算单', trigger: 'change' }],
|
||||
},
|
||||
feeDialog: { visible: false, selected: [] },
|
||||
}),
|
||||
computed: {
|
||||
visible: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
},
|
||||
editable() {
|
||||
return !this.readonly;
|
||||
},
|
||||
title() {
|
||||
return this.readonly ? '查看结算调整单' : this.recordId ? '编辑结算调整单' : '新增结算调整单';
|
||||
},
|
||||
settlementTypeName() {
|
||||
return this.form.settlementType === 'receivable'
|
||||
? '应收'
|
||||
: this.form.settlementType === 'payable'
|
||||
? '应付'
|
||||
: '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
if (value) this.initialize();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.form = createSettlementAdjustmentForm();
|
||||
this.details = [];
|
||||
this.candidates = [];
|
||||
this.feeRows = [];
|
||||
if (!this.recordId) {
|
||||
await this.loadFormalSettlements('');
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
const { data } = await api.getDetail(this.recordId);
|
||||
this.form = { ...createSettlementAdjustmentForm(), ...data };
|
||||
this.details = (data.details || []).map(item => ({
|
||||
...item,
|
||||
adjustmentAmountTax: Number(item.adjustmentAmountTax || 0),
|
||||
adjustmentAmountNoTax:
|
||||
item.adjustmentAmountNoTax == null ? null : Number(item.adjustmentAmountNoTax),
|
||||
}));
|
||||
await this.loadFormalSettlements(this.form.formalSettlementNo || '');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadFormalSettlements(keyword = '') {
|
||||
const { data } = await api.getFormalSettlements(keyword);
|
||||
this.candidates = data?.data || data || [];
|
||||
},
|
||||
async handleFormalChange(id) {
|
||||
const item = this.candidates.find(row => String(row.id) === String(id));
|
||||
if (!item) return;
|
||||
Object.assign(this.form, item, {
|
||||
formalSettlementId: id,
|
||||
formalSettlementNo: item.formalSettlementNo,
|
||||
originalSettlementAmount: Number(item.settlementAmount || 0),
|
||||
settlementTypeName: item.settlementTypeName,
|
||||
});
|
||||
this.details = [];
|
||||
this.recalculate();
|
||||
},
|
||||
async openFeeDialog() {
|
||||
const { data } = await api.getFormalDetails(this.form.formalSettlementId);
|
||||
const used = new Set(this.details.map(item => String(item.formalSettlementDetailFeeId)));
|
||||
this.feeRows = (data?.data || data || []).filter(
|
||||
item => !used.has(String(item.formalSettlementDetailFeeId))
|
||||
);
|
||||
this.feeDialog = { visible: true, selected: [] };
|
||||
},
|
||||
confirmFees() {
|
||||
this.feeDialog.selected.forEach(item =>
|
||||
this.details.push({
|
||||
...item,
|
||||
adjustmentAmountTax: 0,
|
||||
adjustmentAmountNoTax: null,
|
||||
remark: '',
|
||||
})
|
||||
);
|
||||
this.recalculate();
|
||||
this.feeDialog.visible = false;
|
||||
},
|
||||
recalculate() {
|
||||
const total = this.details.reduce(
|
||||
(sum, row) => sum + Number(row.adjustmentAmountTax || 0),
|
||||
0
|
||||
);
|
||||
this.form.adjustmentAmount = Number(total.toFixed(2));
|
||||
this.form.adjustedSettlementAmount = Number(
|
||||
(Number(this.form.originalSettlementAmount || 0) + total).toFixed(2)
|
||||
);
|
||||
},
|
||||
async handleSave() {
|
||||
await this.$refs.formRef.validate();
|
||||
if (!this.details.length) return this.$message.warning('请至少添加一条调整费用');
|
||||
this.saving = true;
|
||||
try {
|
||||
await api.save({
|
||||
id: this.form.id,
|
||||
formalSettlementId: this.form.formalSettlementId,
|
||||
remark: this.form.remark,
|
||||
details: this.details.map(item => ({
|
||||
formalSettlementDetailId: item.formalSettlementDetailId,
|
||||
formalSettlementDetailFeeId: item.formalSettlementDetailFeeId,
|
||||
feeType: item.feeType,
|
||||
feeItem: item.feeItem,
|
||||
adjustmentAmountTax: item.adjustmentAmountTax,
|
||||
adjustmentAmountNoTax: item.adjustmentAmountNoTax,
|
||||
remark: item.remark,
|
||||
})),
|
||||
});
|
||||
this.$message.success('保存成功');
|
||||
this.visible = false;
|
||||
this.$emit('success');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
formatMoney(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settlement-adjustment-editor__section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.settlement-adjustment-editor__section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.settlement-adjustment-editor__summary {
|
||||
padding: 12px 0;
|
||||
text-align: right;
|
||||
}
|
||||
.settlement-adjustment-editor :deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.settlement-adjustment-editor :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
.settlement-adjustment-editor :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||||
background: #fafafa;
|
||||
}
|
||||
.negative {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.dialog-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dialog-section-title::before {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
background: #409eff;
|
||||
content: '';
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,832 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" width="98%" top="2vh" append-to-body destroy-on-close>
|
||||
<div v-loading="loading" class="reconciliation-editor">
|
||||
<section class="reconciliation-editor__section">
|
||||
<div class="dialog-section-title">导入外部账单,与内部账单核对</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-row :gutter="24">
|
||||
<el-col v-for="field in formFields" :key="field.prop" :span="8">
|
||||
<el-form-item :label="field.label" :prop="field.prop">
|
||||
<el-input
|
||||
v-if="field.prop === 'formalSettlementNo' && editable"
|
||||
v-model="form.formalSettlementNo"
|
||||
readonly
|
||||
placeholder="请选择正式结算单"
|
||||
@click="openFormalDialog"
|
||||
>
|
||||
<template #append><el-button @click="openFormalDialog">选择</el-button></template>
|
||||
</el-input>
|
||||
<el-select
|
||||
v-else-if="field.prop === 'reconciliationMode' && editable"
|
||||
v-model="form.reconciliationMode"
|
||||
>
|
||||
<el-option label="整车总额对账" value="vehicle" />
|
||||
<el-option label="货物明细对账" value="cargo" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-else-if="field.type === 'date' && editable"
|
||||
v-model="form.reconciliationDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="field.money"
|
||||
v-model="form[field.prop]"
|
||||
:controls="false"
|
||||
:precision="2"
|
||||
disabled
|
||||
/>
|
||||
<span v-else>{{ displayValue(form[field.prop]) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-if="editable"
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
<span v-else>{{ displayValue(form.remark) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="reconciliation-editor__section">
|
||||
<div class="dialog-section-title">内部账单</div>
|
||||
<div class="reconciliation-editor__stats">
|
||||
<div class="stat-card">
|
||||
<b>{{ form.internalBillCount || 0 }}</b
|
||||
><span>我方账单数</span
|
||||
><small
|
||||
>对方 {{ form.externalBillCount || 0 }} 条 / 差异
|
||||
{{ form.differenceCount || 0 }} 条</small
|
||||
>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<b>{{ formatNumber(form.internalQuantity) }}</b
|
||||
><span>我方货量</span
|
||||
><small
|
||||
>对方 {{ formatNumber(form.externalQuantity) }} / 差异
|
||||
{{ formatNumber(form.differenceQuantity) }}</small
|
||||
>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<b>{{ formatMoney(form.internalAmount) }}</b
|
||||
><span>我方结算金额</span
|
||||
><small
|
||||
>对方 {{ formatMoney(form.externalAmount) }} / 差异
|
||||
{{ formatMoney(form.differenceAmount) }}</small
|
||||
>
|
||||
</div>
|
||||
<div class="stat-card stat-card--success">
|
||||
<b>{{ form.matchedCount || 0 }}</b
|
||||
><span>成功匹配(条)</span>
|
||||
</div>
|
||||
<div class="stat-card stat-card--danger">
|
||||
<b>{{ form.unmatchedCount || 0 }}</b
|
||||
><span>无法匹配(条)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="reconciliation-editor__filter">
|
||||
<el-input v-model="internalQuery.documentNo" placeholder="单据号" clearable />
|
||||
<el-input v-model="internalQuery.vehicleNo" placeholder="车号" clearable />
|
||||
<el-input v-model="internalQuery.batchNo" placeholder="批次号" clearable />
|
||||
<el-input v-model="internalQuery.cargoName" placeholder="货物名称" clearable />
|
||||
<el-button @click="resetInternalQuery">重置</el-button>
|
||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||
</div>
|
||||
<el-table :data="filteredInternalRows" border max-height="420">
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in internalColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="column.prop === 'matchResult'" :type="matchTagType(row.matchResult)">{{
|
||||
matchName(row.matchResult)
|
||||
}}</el-tag>
|
||||
<el-tag
|
||||
v-else-if="column.prop === 'updateResult'"
|
||||
:type="updateTagType(row.updateResult)"
|
||||
>{{ updateName(row.updateResult) }}</el-tag
|
||||
>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="reconciliation-editor__links">
|
||||
<el-link v-if="editable" type="primary" @click="openAdjust(row)">调整</el-link>
|
||||
<el-link
|
||||
v-if="editable && row.matchResult === 'matched'"
|
||||
type="primary"
|
||||
@click="handleUnmatch(row)"
|
||||
>取消匹配</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="editable && row.matchResult !== 'matched'"
|
||||
type="primary"
|
||||
@click="openManualMatch(row)"
|
||||
>人工匹配</el-link
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section class="reconciliation-editor__section">
|
||||
<div class="reconciliation-editor__section-head">
|
||||
<div class="dialog-section-title">导入外部账单</div>
|
||||
<div class="reconciliation-editor__actions" v-if="editable">
|
||||
<el-button type="primary" plain @click="downloadTemplate('vehicle')"
|
||||
>下载整车对账模板</el-button
|
||||
>
|
||||
<el-button type="primary" plain @click="downloadTemplate('cargo')"
|
||||
>下载货物明细对账模板</el-button
|
||||
>
|
||||
<el-button type="primary" plain @click="chooseImport">导入</el-button>
|
||||
<el-button type="primary" @click="handleMatch">开始匹配内部账单</el-button>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".xls,.xlsx"
|
||||
class="hidden-file"
|
||||
@change="handleImport"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs v-model="externalTab">
|
||||
<el-tab-pane :label="`导入明细(${externalDetails.length})`" name="all" />
|
||||
<el-tab-pane :label="`疑似重复(${duplicateRows.length})`" name="duplicate" />
|
||||
</el-tabs>
|
||||
<el-table :data="visibleExternalRows" border max-height="420">
|
||||
<el-table-column
|
||||
v-for="column in externalColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="column.prop === 'matchStatus'" :type="matchTagType(row.matchStatus)">{{
|
||||
matchName(row.matchStatus)
|
||||
}}</el-tag>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right" align="center">
|
||||
<template #default="{ row }"
|
||||
><el-link
|
||||
v-if="editable && row.matchStatus !== 'matched'"
|
||||
type="primary"
|
||||
@click="openManualMatchByExternal(row)"
|
||||
>匹配</el-link
|
||||
></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section class="reconciliation-editor__section">
|
||||
<el-collapse>
|
||||
<el-collapse-item title="操作说明" name="help">
|
||||
<div class="reconciliation-editor__help">
|
||||
<p>1. 新增对账单时选择一张已审批通过的正式结算单,系统自动带出内部账单明细。</p>
|
||||
<p>
|
||||
2. 导入对方 Excel
|
||||
账单后点击“开始匹配内部账单”,系统按车号、货物、地址、批次、发货时间、运输量和金额匹配。
|
||||
</p>
|
||||
<p>3. 外部账单存在重复候选时会标记为疑似重复,需要人工选择唯一明细匹配。</p>
|
||||
<p>4. 只有所有内外部明细一一匹配且差异为 0,才允许按匹配结果更新账单或完成对账。</p>
|
||||
<p>
|
||||
5.
|
||||
已付金额大于外部匹配金额时禁止更新;整车模式一车多货会跳过自动更新,可通过“调整”逐货物修改。
|
||||
</p>
|
||||
<p>6. 按匹配结果更新成功后生成变更记录;完成对账后单据不可再编辑和删除。</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<template v-if="editable">
|
||||
<el-button type="primary" plain :loading="saving" @click="handleSave">保存草稿</el-button>
|
||||
<el-button type="primary" plain :loading="actionLoading" @click="handleUpdate"
|
||||
>按匹配结果更新账单</el-button
|
||||
>
|
||||
<el-button type="primary" :loading="actionLoading" @click="handleComplete"
|
||||
>完成对账</el-button
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="formalDialog.visible" title="选择正式结算单" width="84%" append-to-body>
|
||||
<el-form :model="formalDialog.query" inline label-position="right" label-width="160px">
|
||||
<el-form-item label="正式结算单号"
|
||||
><el-input v-model="formalDialog.query.keyword" clearable
|
||||
/></el-form-item>
|
||||
<el-form-item><el-button @click="loadFormalOptions">查询</el-button></el-form-item>
|
||||
</el-form>
|
||||
<el-table
|
||||
v-loading="formalDialog.loading"
|
||||
:data="formalDialog.rows"
|
||||
border
|
||||
@selection-change="formalDialog.selected = $event"
|
||||
>
|
||||
<el-table-column type="selection" width="52" :selectable="row => !row.isSelected" />
|
||||
<el-table-column type="index" label="序号" width="64" />
|
||||
<el-table-column prop="formalSettlementNo" label="正式结算单号" min-width="150" />
|
||||
<el-table-column prop="contractNo" label="合同编号" min-width="130" />
|
||||
<el-table-column prop="contractName" label="合同名称" min-width="160" />
|
||||
<el-table-column prop="payerName" label="付款方" min-width="150" />
|
||||
<el-table-column prop="payeeName" label="收款方" min-width="150" />
|
||||
<el-table-column prop="settlementAmount" label="结算金额" min-width="120"
|
||||
><template #default="{ row }">{{
|
||||
formatMoney(row.settlementAmount, row.currency)
|
||||
}}</template></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<div class="reconciliation-editor__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="formalDialog.page.current"
|
||||
v-model:page-size="formalDialog.page.size"
|
||||
:total="formalDialog.page.total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="loadFormalOptions"
|
||||
/>
|
||||
</div>
|
||||
<template #footer
|
||||
><el-button @click="formalDialog.visible = false">取消</el-button
|
||||
><el-button type="primary" @click="confirmFormal">确定</el-button></template
|
||||
>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="adjustDialog.visible" title="结算明细调整" width="90%" append-to-body>
|
||||
<el-table :data="adjustDialog.rows" border>
|
||||
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
|
||||
<el-table-column prop="cargoType" label="货物类型" min-width="120" />
|
||||
<el-table-column prop="transportQuantity" label="运输量" min-width="120"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.transportQuantity"
|
||||
:min="0"
|
||||
:controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column prop="unitPrice" label="运输单价" min-width="120"
|
||||
><template #default="{ row }"
|
||||
><el-input-number v-model="row.unitPrice" :min="0" :controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column prop="freightAmount" label="运输费" min-width="120"
|
||||
><template #default="{ row }"
|
||||
><el-input-number v-model="row.freightAmount" :min="0" :controls="false" /></template
|
||||
></el-table-column>
|
||||
<el-table-column prop="settlementAmount" label="结算金额" min-width="140"
|
||||
><template #default="{ row }"
|
||||
><el-input-number v-model="row.settlementAmount" :min="0" :controls="false" /></template
|
||||
></el-table-column>
|
||||
</el-table>
|
||||
<el-form class="dialog-form" label-position="right" label-width="auto"
|
||||
><el-form-item label="调整原因" required
|
||||
><el-input
|
||||
v-model="adjustDialog.reason"
|
||||
type="textarea"
|
||||
maxlength="200"
|
||||
show-word-limit /></el-form-item
|
||||
></el-form>
|
||||
<template #footer
|
||||
><el-button @click="adjustDialog.visible = false">取消</el-button
|
||||
><el-button type="primary" :loading="adjustDialog.saving" @click="saveAdjust"
|
||||
>保存</el-button
|
||||
></template
|
||||
>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="manualDialog.visible" title="选择外部账单明细" width="86%" append-to-body>
|
||||
<el-table :data="unmatchedExternalRows" border @row-click="manualDialog.selected = $event">
|
||||
<el-table-column type="index" label="序号" width="64" />
|
||||
<el-table-column prop="externalLineNo" label="外部行号" width="90" />
|
||||
<el-table-column prop="vehicleNo" label="车牌号" width="110" />
|
||||
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
|
||||
<el-table-column prop="transportQuantity" label="运输量" width="110" />
|
||||
<el-table-column prop="settlementAmount" label="结算金额" width="130"
|
||||
><template #default="{ row }">{{
|
||||
formatMoney(row.settlementAmount)
|
||||
}}</template></el-table-column
|
||||
>
|
||||
<el-table-column label="选择" width="80"
|
||||
><template #default="{ row }"
|
||||
><el-radio v-model="manualDialog.selected" :label="row"> </el-radio></template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<template #footer
|
||||
><el-button @click="manualDialog.visible = false">取消</el-button
|
||||
><el-button type="primary" @click="confirmManualMatch">确定</el-button></template
|
||||
>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/settlement/transportReconciliation';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm';
|
||||
import {
|
||||
internalColumns,
|
||||
externalCargoColumns,
|
||||
externalVehicleColumns,
|
||||
} from '@/option/settlement/transportReconciliationTable';
|
||||
|
||||
export default {
|
||||
name: 'TransportReconciliationEditor',
|
||||
props: {
|
||||
modelValue: Boolean,
|
||||
recordId: [String, Number],
|
||||
settlementType: { type: String, default: 'payable' },
|
||||
readonly: Boolean,
|
||||
},
|
||||
emits: ['update:modelValue', 'success'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
saving: false,
|
||||
actionLoading: false,
|
||||
currentId: null,
|
||||
form: this.emptyForm(),
|
||||
formFields: transportReconciliationFormFields,
|
||||
internalColumns,
|
||||
internalDetails: [],
|
||||
externalDetails: [],
|
||||
internalQuery: { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' },
|
||||
internalFilterTick: 0,
|
||||
externalTab: 'all',
|
||||
formalDialog: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
rows: [],
|
||||
selected: [],
|
||||
query: { keyword: '' },
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
},
|
||||
adjustDialog: { visible: false, saving: false, rows: [], reason: '' },
|
||||
manualDialog: { visible: false, internal: null, selected: null },
|
||||
rules: {
|
||||
formalSettlementNo: [{ required: true, message: '请选择正式结算单', trigger: 'change' }],
|
||||
reconciliationMode: [{ required: true, message: '请选择对账模式', trigger: 'change' }],
|
||||
remark: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visible: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
},
|
||||
editable() {
|
||||
return !this.readonly;
|
||||
},
|
||||
title() {
|
||||
return this.readonly
|
||||
? '查看运输对账单'
|
||||
: this.currentId
|
||||
? '编辑运输对账单'
|
||||
: '新增运输对账单';
|
||||
},
|
||||
filteredInternalRows() {
|
||||
void this.internalFilterTick;
|
||||
const query = this.internalQuery;
|
||||
return this.internalDetails.filter(
|
||||
row =>
|
||||
(!query.documentNo || String(row.documentNo || '').includes(query.documentNo)) &&
|
||||
(!query.vehicleNo || String(row.vehicleNo || '').includes(query.vehicleNo)) &&
|
||||
(!query.batchNo || String(row.batchNo || '').includes(query.batchNo)) &&
|
||||
(!query.cargoName || String(row.cargoName || '').includes(query.cargoName))
|
||||
);
|
||||
},
|
||||
externalColumns() {
|
||||
return this.form.reconciliationMode === 'cargo'
|
||||
? externalCargoColumns
|
||||
: externalVehicleColumns;
|
||||
},
|
||||
duplicateRows() {
|
||||
return this.externalDetails.filter(
|
||||
row => row.suspectedDuplicate || row.matchStatus === 'suspected_duplicate'
|
||||
);
|
||||
},
|
||||
visibleExternalRows() {
|
||||
return this.externalTab === 'duplicate' ? this.duplicateRows : this.externalDetails;
|
||||
},
|
||||
unmatchedExternalRows() {
|
||||
return this.externalDetails.filter(
|
||||
row => row.matchStatus !== 'matched' && !row.suspectedDuplicate
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
if (value) this.initialize();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
emptyForm() {
|
||||
return {
|
||||
reconciliationNo: '',
|
||||
reconciliationMode: 'vehicle',
|
||||
formalSettlementId: null,
|
||||
formalSettlementNo: '',
|
||||
payerName: '',
|
||||
payeeName: '',
|
||||
projectName: '',
|
||||
deptName: '',
|
||||
contractName: '',
|
||||
paidAmount: 0,
|
||||
reconcilerName: '',
|
||||
reconciliationDate: '',
|
||||
remark: '',
|
||||
internalBillCount: 0,
|
||||
externalBillCount: 0,
|
||||
differenceCount: 0,
|
||||
internalQuantity: 0,
|
||||
externalQuantity: 0,
|
||||
differenceQuantity: 0,
|
||||
internalAmount: 0,
|
||||
externalAmount: 0,
|
||||
differenceAmount: 0,
|
||||
matchedCount: 0,
|
||||
unmatchedCount: 0,
|
||||
};
|
||||
},
|
||||
async initialize() {
|
||||
this.currentId = this.recordId;
|
||||
this.form = this.emptyForm();
|
||||
this.internalDetails = [];
|
||||
this.externalDetails = [];
|
||||
this.externalTab = 'all';
|
||||
this.internalQuery = { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' };
|
||||
if (!this.currentId) {
|
||||
this.form.reconciliationMode = 'vehicle';
|
||||
this.form.reconcilerName = '当前用户';
|
||||
this.form.reconciliationDate = this.$dayjs().format('YYYY-MM-DD');
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
await this.loadDetail();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadDetail() {
|
||||
const { data } = await api.getDetail(this.currentId);
|
||||
this.form = { ...this.emptyForm(), ...data };
|
||||
this.internalDetails = data.internalDetails || [];
|
||||
this.externalDetails = data.externalDetails || [];
|
||||
},
|
||||
openFormalDialog() {
|
||||
if (!this.editable) return;
|
||||
this.formalDialog.visible = true;
|
||||
this.formalDialog.page.current = 1;
|
||||
this.loadFormalOptions();
|
||||
},
|
||||
async loadFormalOptions() {
|
||||
this.formalDialog.loading = true;
|
||||
try {
|
||||
const { data } = await api.getFormalOptions(
|
||||
this.formalDialog.page.current,
|
||||
this.formalDialog.page.size,
|
||||
{ settlementType: this.settlementType, keyword: this.formalDialog.query.keyword }
|
||||
);
|
||||
this.formalDialog.rows = data.records || [];
|
||||
this.formalDialog.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.formalDialog.loading = false;
|
||||
}
|
||||
},
|
||||
confirmFormal() {
|
||||
if (this.formalDialog.selected.length !== 1)
|
||||
return this.$message.warning('请选择一张正式结算单');
|
||||
const selected = this.formalDialog.selected[0];
|
||||
this.form = {
|
||||
...this.form,
|
||||
...selected,
|
||||
formalSettlementId: selected.id,
|
||||
formalSettlementNo: selected.formalSettlementNo,
|
||||
reconciliationNo: this.form.reconciliationNo,
|
||||
};
|
||||
this.formalDialog.visible = false;
|
||||
},
|
||||
async handleSave() {
|
||||
const valid = await this.$refs.formRef.validate().catch(() => false);
|
||||
if (!valid || !this.form.formalSettlementId) return this.$message.warning('请选择正式结算单');
|
||||
this.saving = true;
|
||||
try {
|
||||
const { data } = await api.save({
|
||||
id: this.currentId,
|
||||
formalSettlementId: this.form.formalSettlementId,
|
||||
reconciliationMode: this.form.reconciliationMode,
|
||||
reconciliationDate: this.form.reconciliationDate,
|
||||
remark: this.form.remark,
|
||||
});
|
||||
this.currentId = data;
|
||||
await this.loadDetail();
|
||||
this.$message.success('草稿保存成功');
|
||||
this.$emit('success');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
async handleMatch() {
|
||||
if (!this.currentId) {
|
||||
await this.handleSave();
|
||||
if (!this.currentId) return;
|
||||
}
|
||||
this.actionLoading = true;
|
||||
try {
|
||||
await api.match(this.currentId);
|
||||
await this.loadDetail();
|
||||
this.$message.success('匹配完成');
|
||||
} finally {
|
||||
this.actionLoading = false;
|
||||
}
|
||||
},
|
||||
async handleUpdate() {
|
||||
if (!this.currentId) return this.$message.warning('请先保存对账单');
|
||||
await this.$confirm('将以外部账单匹配金额更新内部结算明细,是否继续?', '更新账单', {
|
||||
type: 'warning',
|
||||
});
|
||||
this.actionLoading = true;
|
||||
try {
|
||||
await api.updateByMatch(this.currentId);
|
||||
await this.loadDetail();
|
||||
this.$message.success('账单更新完成');
|
||||
} finally {
|
||||
this.actionLoading = false;
|
||||
}
|
||||
},
|
||||
async handleComplete() {
|
||||
if (!this.currentId) return this.$message.warning('请先保存对账单');
|
||||
await this.$confirm('完成后对账单不可修改和删除,是否继续?', '完成对账', {
|
||||
type: 'warning',
|
||||
});
|
||||
this.actionLoading = true;
|
||||
try {
|
||||
await api.complete(this.currentId);
|
||||
await this.loadDetail();
|
||||
this.$message.success('对账完成');
|
||||
this.$emit('success');
|
||||
} finally {
|
||||
this.actionLoading = false;
|
||||
}
|
||||
},
|
||||
chooseImport() {
|
||||
if (!this.currentId) return this.$message.warning('请先保存对账单');
|
||||
this.$refs.fileInput?.click();
|
||||
},
|
||||
async handleImport(event) {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = '';
|
||||
if (!file) return;
|
||||
try {
|
||||
const response =
|
||||
this.form.reconciliationMode === 'cargo'
|
||||
? await api.importCargo(this.currentId, file)
|
||||
: await api.importVehicle(this.currentId, file);
|
||||
const contentType = response.headers?.['content-type'] || response.data?.type || '';
|
||||
if (contentType.includes('spreadsheetml') || contentType.includes('ms-excel')) {
|
||||
downloadXls(
|
||||
response.data,
|
||||
`运输对账导入失败明细${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`
|
||||
);
|
||||
this.$message.warning('部分数据导入失败,已下载失败明细');
|
||||
} else {
|
||||
const result = JSON.parse(await response.data.text());
|
||||
if (result.code !== 200) throw new Error(result.msg || '导入失败');
|
||||
this.$message.success('外部账单导入成功');
|
||||
}
|
||||
await this.loadDetail();
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '外部账单导入失败');
|
||||
}
|
||||
},
|
||||
async downloadTemplate(mode) {
|
||||
const response = await api.template(mode);
|
||||
downloadXls(
|
||||
response.data,
|
||||
`${mode === 'cargo' ? '货物明细对账模板' : '整车总额对账模板'}.xlsx`
|
||||
);
|
||||
},
|
||||
resetInternalQuery() {
|
||||
this.internalQuery = { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' };
|
||||
this.internalFilterTick++;
|
||||
},
|
||||
openAdjust(row) {
|
||||
this.adjustDialog = {
|
||||
visible: true,
|
||||
saving: false,
|
||||
reason: '',
|
||||
rows: this.internalDetails
|
||||
.filter(item => item.formalSettlementDetailId === row.formalSettlementDetailId)
|
||||
.map(item => ({ ...item })),
|
||||
};
|
||||
},
|
||||
async saveAdjust() {
|
||||
if (!this.adjustDialog.reason.trim()) return this.$message.warning('请输入调整原因');
|
||||
this.adjustDialog.saving = true;
|
||||
try {
|
||||
for (const row of this.adjustDialog.rows)
|
||||
await api.adjust({
|
||||
...row,
|
||||
reconciliationId: this.currentId,
|
||||
updateMessage: this.adjustDialog.reason,
|
||||
});
|
||||
this.adjustDialog.visible = false;
|
||||
await this.loadDetail();
|
||||
this.$message.success('调整保存成功');
|
||||
} finally {
|
||||
this.adjustDialog.saving = false;
|
||||
}
|
||||
},
|
||||
async handleUnmatch(row) {
|
||||
await api.unmatch(row.id);
|
||||
await this.loadDetail();
|
||||
},
|
||||
openManualMatch(row) {
|
||||
this.manualDialog = { visible: true, internal: row, selected: null };
|
||||
},
|
||||
openManualMatchByExternal(row) {
|
||||
this.manualDialog = { visible: true, internal: null, selected: row };
|
||||
},
|
||||
async confirmManualMatch() {
|
||||
if (!this.manualDialog.selected) return this.$message.warning('请选择外部账单明细');
|
||||
if (!this.manualDialog.internal) return this.$message.warning('请从内部账单行发起人工匹配');
|
||||
await api.manualMatch({
|
||||
reconciliationId: this.currentId,
|
||||
internalId: this.manualDialog.internal.id,
|
||||
externalId: this.manualDialog.selected.id,
|
||||
});
|
||||
this.manualDialog.visible = false;
|
||||
await this.loadDetail();
|
||||
this.$message.success('人工匹配成功');
|
||||
},
|
||||
matchName(value) {
|
||||
return (
|
||||
{
|
||||
matched: '已匹配',
|
||||
suspected_duplicate: '疑似重复',
|
||||
partial: '部分匹配',
|
||||
unmatched: '未匹配',
|
||||
}[value] || '未匹配'
|
||||
);
|
||||
},
|
||||
matchTagType(value) {
|
||||
return value === 'matched'
|
||||
? 'success'
|
||||
: value === 'suspected_duplicate'
|
||||
? 'danger'
|
||||
: value === 'partial'
|
||||
? 'warning'
|
||||
: 'info';
|
||||
},
|
||||
updateName(value) {
|
||||
return (
|
||||
{
|
||||
updated: '已更新',
|
||||
skipped_multi_cargo: '跳过更新',
|
||||
manually_adjusted: '手工调整',
|
||||
not_updated: '未更新',
|
||||
}[value] ||
|
||||
value ||
|
||||
'未更新'
|
||||
);
|
||||
},
|
||||
updateTagType(value) {
|
||||
return value === 'updated' || value === 'manually_adjusted'
|
||||
? 'success'
|
||||
: value === 'skipped_multi_cargo'
|
||||
? 'warning'
|
||||
: 'info';
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
formatMoney(value, currency = 'RMB') {
|
||||
return `${Number(value || 0).toFixed(2)} ${currency || 'RMB'}`;
|
||||
},
|
||||
formatNumber(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.reconciliation-editor__section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.reconciliation-editor__section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.reconciliation-editor__actions,
|
||||
.reconciliation-editor__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.reconciliation-editor__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.stat-card {
|
||||
min-height: 104px;
|
||||
border: 1px solid #eff1f7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
background: #fff;
|
||||
}
|
||||
.stat-card b {
|
||||
font-size: 20px;
|
||||
color: #303133;
|
||||
}
|
||||
.stat-card span {
|
||||
color: #606266;
|
||||
}
|
||||
.stat-card small {
|
||||
color: #909399;
|
||||
}
|
||||
.stat-card--success b {
|
||||
color: #67c23a;
|
||||
}
|
||||
.stat-card--danger b {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.reconciliation-editor__filter {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto auto;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.reconciliation-editor__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.reconciliation-editor__help {
|
||||
color: #606266;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.dialog-form {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.hidden-file {
|
||||
display: none;
|
||||
}
|
||||
.reconciliation-editor :deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.reconciliation-editor :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
.reconciliation-editor :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||||
background: #fafafa;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.reconciliation-editor__stats {
|
||||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||||
}
|
||||
.reconciliation-editor__filter {
|
||||
grid-template-columns: repeat(2, minmax(160px, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,604 @@
|
||||
<template>
|
||||
<basic-container class="formal-page">
|
||||
<section class="formal-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
|
||||
<div class="formal-page__search-grid">
|
||||
<el-form-item v-for="field in visibleSearchFields" :key="field.prop" :label="field.label">
|
||||
<el-date-picker
|
||||
v-if="field.type === 'daterange'"
|
||||
v-model="query[field.prop]"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD"
|
||||
range-separator="~"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'select'"
|
||||
v-model="query[field.prop]"
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in field.options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<div class="formal-page__search-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
|
||||
{{ searchExpanded ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="formal-page__table-panel">
|
||||
<el-tabs v-model="activeSettlementType" @tab-change="handleSettlementTypeChange">
|
||||
<el-tab-pane label="应付" name="payable" />
|
||||
<el-tab-pane label="应收" name="receivable" />
|
||||
</el-tabs>
|
||||
<div class="formal-page__toolbar">
|
||||
<div>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_add')"
|
||||
type="primary"
|
||||
@click="openCreate"
|
||||
>新增</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_payment')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openPaymentDialog"
|
||||
>付款申请</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_sync')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleSync"
|
||||
>同步金蝶</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_print')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handlePrint"
|
||||
>打印结算单</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_export')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleExport"
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="formal-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border @selection-change="selection = $event">
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="column.link && row[column.prop]" type="primary" @click="openView(row)">{{
|
||||
row[column.prop]
|
||||
}}</el-link>
|
||||
<el-tag v-else-if="column.status" :type="statusType(row.approvalStatus)">{{
|
||||
displayValue(row[column.prop])
|
||||
}}</el-tag>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop], row.currency) }}</span>
|
||||
<span v-else-if="column.prop === 'invoiceStatusName'">{{
|
||||
invoiceName(row.invoiceStatus)
|
||||
}}</span>
|
||||
<span v-else-if="column.prop === 'paymentStatusName'">{{
|
||||
paymentName(row.paymentStatus)
|
||||
}}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right" align="center">
|
||||
<template #default="{ row }"
|
||||
><div class="formal-page__links">
|
||||
<el-link
|
||||
v-if="hasPermission('formal_settlement_view')"
|
||||
type="primary"
|
||||
@click="openView(row)"
|
||||
>查看</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('formal_settlement_edit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="openEdit(row)"
|
||||
>编辑</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('formal_settlement_delete') && row.approvalStatus === 'draft'"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('formal_settlement_submit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="handleSubmit(row)"
|
||||
>提交</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleApprove(row)"
|
||||
>通过</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleReturn(row)"
|
||||
>驳回</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('formal_settlement_void') &&
|
||||
row.approvalStatus === 'approved' &&
|
||||
row.kingdeeSyncStatus !== 'synced'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleVoid(row)"
|
||||
>作废</el-link
|
||||
>
|
||||
</div></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="formal-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadTable"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<formal-settlement-editor
|
||||
v-model="editor.visible"
|
||||
:record-id="editor.id"
|
||||
:readonly="editor.readonly"
|
||||
@success="loadTable"
|
||||
/>
|
||||
<el-dialog v-model="paymentDialog.visible" title="付款申请" width="520px" append-to-body>
|
||||
<el-form :model="paymentDialog.form" label-position="right" label-width="auto">
|
||||
<el-form-item label="结算单号">{{
|
||||
paymentDialog.row.formalSettlementNo || '-'
|
||||
}}</el-form-item>
|
||||
<el-form-item label="剩余可申请金额">{{
|
||||
formatMoney(paymentAvailable, paymentDialog.row.currency)
|
||||
}}</el-form-item>
|
||||
<el-form-item label="申请付款金额" required
|
||||
><el-input-number
|
||||
v-model="paymentDialog.form.appliedAmount"
|
||||
:min="0.01"
|
||||
:max="paymentAvailable"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
/></el-form-item>
|
||||
<el-form-item label="备注"
|
||||
><el-input
|
||||
v-model="paymentDialog.form.remark"
|
||||
type="textarea"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/></el-form-item>
|
||||
</el-form>
|
||||
<template #footer
|
||||
><el-button @click="paymentDialog.visible = false">取消</el-button
|
||||
><el-button type="primary" :loading="paymentDialog.loading" @click="submitPayment"
|
||||
>提交</el-button
|
||||
></template
|
||||
>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as XLSX from 'xlsx';
|
||||
import * as api from '@/api/settlement/formalSettlement';
|
||||
import {
|
||||
formalSettlementSearchFields,
|
||||
invoiceStatusOptions,
|
||||
paymentStatusOptions,
|
||||
} from '@/option/settlement/formalSettlementSearch';
|
||||
import { formalSettlementTableColumns } from '@/option/settlement/formalSettlementTable';
|
||||
import FormalSettlementEditor from './components/formal-settlement-editor.vue';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
formalSettlementNo: '',
|
||||
preSettlementNo: '',
|
||||
projectName: '',
|
||||
deptName: '',
|
||||
contractNo: '',
|
||||
payerName: '',
|
||||
payeeName: '',
|
||||
invoiceStatus: '',
|
||||
paymentStatus: '',
|
||||
approvalStatus: '',
|
||||
kingdeeSyncStatus: '',
|
||||
createDateRange: [],
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'FormalSettlement',
|
||||
components: { FormalSettlementEditor },
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
query: emptyQuery(),
|
||||
searchExpanded: false,
|
||||
activeSettlementType: 'payable',
|
||||
searchFields: formalSettlementSearchFields,
|
||||
columns: formalSettlementTableColumns,
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
editor: { visible: false, id: null, readonly: false },
|
||||
paymentDialog: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
row: {},
|
||||
form: { appliedAmount: 0, remark: '' },
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
paymentAvailable() {
|
||||
return Math.max(
|
||||
0,
|
||||
Number(this.paymentDialog.row.settlementAmount || 0) -
|
||||
Number(this.paymentDialog.row.appliedPaymentAmount || 0)
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadTable();
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
return this.permission?.[code] !== false;
|
||||
},
|
||||
async loadTable() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = this.buildQueryParams();
|
||||
const response = await api.getList(this.page.current, this.page.size, params);
|
||||
const data = this.unwrapData(response);
|
||||
this.rows = data.records || [];
|
||||
this.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
resetSearch() {
|
||||
this.query = emptyQuery();
|
||||
this.handleSearch();
|
||||
},
|
||||
toggleSearch() {
|
||||
this.searchExpanded = !this.searchExpanded;
|
||||
},
|
||||
handleSizeChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
openCreate() {
|
||||
this.editor = { visible: true, id: null, readonly: false };
|
||||
},
|
||||
openEdit(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: false };
|
||||
},
|
||||
openView(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: true };
|
||||
},
|
||||
async handleDelete(row) {
|
||||
await this.$confirm('确认删除该正式结算草稿?', '提示', { type: 'warning' });
|
||||
await api.remove(row.id);
|
||||
this.$message.success('删除成功');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleSubmit(row) {
|
||||
await this.$confirm('提交后来源预结算将保持锁定,确认提交?', '提示', { type: 'warning' });
|
||||
await api.submit({ id: row.id });
|
||||
this.$message.success('提交成功');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm(
|
||||
'审核通过后正式结算生效,金额只能通过结算调整单处理,确认通过?',
|
||||
'提示',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
await api.approve({ id: row.id });
|
||||
this.$message.success('审批通过');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleReturn(row) {
|
||||
const { value } = await this.$prompt('请输入驳回原因', '审批驳回', {
|
||||
inputValidator: value => Boolean(value?.trim()) || '请输入驳回原因',
|
||||
});
|
||||
await api.returnBill({ id: row.id, reason: value });
|
||||
this.$message.success('已驳回');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleVoid(row) {
|
||||
const { value } = await this.$prompt('请输入作废原因', '作废正式结算单', {
|
||||
inputValidator: value => Boolean(value?.trim()) || '请输入作废原因',
|
||||
});
|
||||
await api.voidBill({ id: row.id, reason: value });
|
||||
this.$message.success('已作废');
|
||||
this.loadTable();
|
||||
},
|
||||
selectedOne(action) {
|
||||
if (this.selection.length !== 1) {
|
||||
this.$message.warning(`${action}需选择一条正式结算单`);
|
||||
return null;
|
||||
}
|
||||
return this.selection[0];
|
||||
},
|
||||
async handleSync() {
|
||||
const row = this.selectedOne('同步金蝶');
|
||||
if (!row) return;
|
||||
const response = await api.syncKingdee(row.id);
|
||||
const data = this.unwrapData(response);
|
||||
this.$message.success(`同步成功,金蝶单据号:${data}`);
|
||||
this.loadTable();
|
||||
},
|
||||
openPaymentDialog() {
|
||||
const row = this.selectedOne('付款申请');
|
||||
if (!row) return;
|
||||
if (row.approvalStatus !== 'approved' || row.settlementType !== 'payable')
|
||||
return this.$message.warning('仅审批通过的应付正式结算单允许发起付款申请');
|
||||
this.paymentDialog = {
|
||||
visible: true,
|
||||
loading: false,
|
||||
row,
|
||||
form: { appliedAmount: 0, remark: '' },
|
||||
};
|
||||
},
|
||||
async submitPayment() {
|
||||
if (Number(this.paymentDialog.form.appliedAmount || 0) <= 0)
|
||||
return this.$message.warning('请输入申请付款金额');
|
||||
this.paymentDialog.loading = true;
|
||||
try {
|
||||
const response = await api.applyPayment({
|
||||
id: this.paymentDialog.row.id,
|
||||
...this.paymentDialog.form,
|
||||
});
|
||||
const data = this.unwrapData(response);
|
||||
this.$message.success(`付款申请已生成:${data}`);
|
||||
this.paymentDialog.visible = false;
|
||||
this.loadTable();
|
||||
} finally {
|
||||
this.paymentDialog.loading = false;
|
||||
}
|
||||
},
|
||||
handlePrint() {
|
||||
const row = this.selectedOne('打印');
|
||||
if (!row) return;
|
||||
const win = window.open('', '_blank');
|
||||
if (!win) return this.$message.warning('浏览器阻止了打印窗口,请允许弹窗后重试');
|
||||
win.document.write(
|
||||
`<!doctype html><html><head><title>${
|
||||
row.formalSettlementNo
|
||||
}</title><style>body{font-family:Arial,"Microsoft YaHei",sans-serif;padding:32px;color:#222}h1{text-align:center}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px 28px;margin-top:30px}.item{border-bottom:1px solid #ddd;padding:8px 0}.actions{text-align:center;margin-top:36px}@media print{.actions{display:none}}</style></head><body><h1>正式结算单</h1><div class="grid"><div class="item">结算单号:${this.escapeHtml(
|
||||
row.formalSettlementNo
|
||||
)}</div><div class="item">预结算单号:${this.escapeHtml(
|
||||
row.preSettlementNos
|
||||
)}</div><div class="item">项目:${this.escapeHtml(
|
||||
row.projectName
|
||||
)}</div><div class="item">合同编号:${this.escapeHtml(
|
||||
row.contractNo
|
||||
)}</div><div class="item">合同名称:${this.escapeHtml(
|
||||
row.contractName
|
||||
)}</div><div class="item">所属组织:${this.escapeHtml(
|
||||
row.deptName
|
||||
)}</div><div class="item">付款方:${this.escapeHtml(
|
||||
row.payerName
|
||||
)}</div><div class="item">收款方:${this.escapeHtml(
|
||||
row.payeeName
|
||||
)}</div><div class="item">结算金额:${this.formatMoney(
|
||||
row.settlementAmount,
|
||||
row.currency
|
||||
)}</div></div><div class="actions"><button onclick="window.print()">打印</button></div></body></html>`
|
||||
);
|
||||
win.document.close();
|
||||
},
|
||||
async handleExport() {
|
||||
const response = await api.getList(1, 100000, this.buildQueryParams());
|
||||
const data = this.unwrapData(response);
|
||||
const rows = (data.records || []).map(item => ({
|
||||
结算单号: item.formalSettlementNo,
|
||||
预结算单号: item.preSettlementNos,
|
||||
来源: item.sourceType,
|
||||
付款方: item.payerName,
|
||||
收款方: item.payeeName,
|
||||
项目名称: item.projectName,
|
||||
所属组织: item.deptName,
|
||||
合同编号: item.contractNo,
|
||||
合同名称: item.contractName,
|
||||
原币结算金额: item.settlementAmount,
|
||||
本位币结算金额: item.localSettlementAmount,
|
||||
结算汇率: item.exchangeRate,
|
||||
发票状态: this.invoiceName(item.invoiceStatus),
|
||||
收付款状态: this.paymentName(item.paymentStatus),
|
||||
审核状态: item.approvalStatusName,
|
||||
金蝶单据号: item.kingdeeBillNo,
|
||||
创建人: item.createUserName,
|
||||
创建时间: item.createTime,
|
||||
}));
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(rows), '正式结算单');
|
||||
XLSX.writeFile(workbook, `正式结算单${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`);
|
||||
},
|
||||
buildQueryParams() {
|
||||
const params = { ...this.query };
|
||||
params.settlementType = this.activeSettlementType;
|
||||
const range = params.createDateRange || [];
|
||||
delete params.createDateRange;
|
||||
if (range.length === 2) {
|
||||
params.createStartDate = range[0];
|
||||
params.createEndDate = range[1];
|
||||
}
|
||||
return params;
|
||||
},
|
||||
unwrapData(response) {
|
||||
const body = response?.data || response || {};
|
||||
return body?.data || body;
|
||||
},
|
||||
handleSettlementTypeChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
escapeHtml(value) {
|
||||
return String(value || '-').replace(
|
||||
/[&<>"']/g,
|
||||
char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])
|
||||
);
|
||||
},
|
||||
isEditable(row) {
|
||||
return ['draft', 'returned'].includes(row.approvalStatus);
|
||||
},
|
||||
statusType(status) {
|
||||
return (
|
||||
{ approved: 'success', reviewing: 'warning', returned: 'danger', voided: 'info' }[status] ||
|
||||
''
|
||||
);
|
||||
},
|
||||
invoiceName(value) {
|
||||
return invoiceStatusOptions.find(item => item.value === value)?.label || value || '-';
|
||||
},
|
||||
paymentName(value) {
|
||||
return paymentStatusOptions.find(item => item.value === value)?.label || value || '-';
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
formatMoney(value, currency = 'RMB') {
|
||||
return `${Number(value || 0).toFixed(2)} ${currency || 'RMB'}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.formal-page__search {
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.formal-page__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px 24px;
|
||||
align-items: start;
|
||||
}
|
||||
.formal-page__search :deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.formal-page__search :deep(.el-form-item__label) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.formal-page__search :deep(.el-input),
|
||||
.formal-page__search :deep(.el-select),
|
||||
.formal-page__search :deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
.formal-page__search-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.formal-page__table-panel {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.formal-page__toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.formal-page__toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-page__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-page__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.formal-page :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
.formal-page :deep(.el-table__body tr:nth-child(even) > td.el-table__cell),
|
||||
.formal-page :deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--left),
|
||||
.formal-page :deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--right) {
|
||||
background: #fafafa;
|
||||
}
|
||||
:deep(.formal-page.basic-container .basic-container__card > .el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.formal-page__search-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.formal-page__search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,805 @@
|
||||
<template>
|
||||
<basic-container class="pre-settlement-page">
|
||||
<section class="pre-settlement-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
|
||||
<div class="pre-settlement-page__search-grid">
|
||||
<template v-for="field in visibleSearchFields" :key="field.prop">
|
||||
<el-form-item :label="field.label">
|
||||
<el-date-picker
|
||||
v-if="field.type === 'daterange'"
|
||||
v-model="query[field.prop]"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD"
|
||||
range-separator="~"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'select'"
|
||||
v-model="query[field.prop]"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in field.options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<div class="pre-settlement-page__search-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
|
||||
{{ searchExpanded ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="pre-settlement-page__table-panel">
|
||||
<div class="pre-settlement-page__toolbar">
|
||||
<div class="pre-settlement-page__toolbar-left">
|
||||
<el-button v-if="hasPermission('pre_settlement_add')" type="primary" @click="openCreate">
|
||||
新增
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('pre_settlement_advance')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openAdvanceDialog"
|
||||
>
|
||||
预付申请
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('pre_settlement_formal')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleFormalSettlement"
|
||||
>
|
||||
尾款结算
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('pre_settlement_print')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="openPrintDialog"
|
||||
>
|
||||
打印结算单
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('pre_settlement_export')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleExport"
|
||||
>
|
||||
导出
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="pre-settlement-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
border
|
||||
class="pre-settlement-page__table"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in tableColumns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
:min-width="column.minWidth"
|
||||
:fixed="column.fixed"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="column.link && row[column.prop] && hasPermission('pre_settlement_view')"
|
||||
type="primary"
|
||||
@click="openView(row)"
|
||||
>
|
||||
{{ row[column.prop] }}
|
||||
</el-link>
|
||||
<el-tag v-else-if="column.status" :type="statusTagType(row.approvalStatus)">
|
||||
{{ row[column.prop] || '-' }}
|
||||
</el-tag>
|
||||
<span v-else-if="column.money">
|
||||
{{ formatMoney(row[column.prop], moneyCurrency(row, column.prop)) }}
|
||||
</span>
|
||||
<span v-else-if="column.precision !== undefined">
|
||||
{{ formatNumber(row[column.prop], column.precision) }}
|
||||
</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="pre-settlement-page__links">
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_view')"
|
||||
type="primary"
|
||||
@click="openView(row)"
|
||||
>
|
||||
查看
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_edit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_delete') && row.approvalStatus === 'draft'"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_view') && row.approvalStatus !== 'draft'"
|
||||
type="primary"
|
||||
@click="openFlow(row)"
|
||||
>
|
||||
流程
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_approve') && row.approvalStatus === 'reviewing'"
|
||||
type="primary"
|
||||
@click="handleApprove(row)"
|
||||
>
|
||||
通过
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_approve') && row.approvalStatus === 'reviewing'"
|
||||
type="danger"
|
||||
@click="handleReturn(row)"
|
||||
>
|
||||
驳回
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('pre_settlement_void') &&
|
||||
row.approvalStatus === 'approved' &&
|
||||
!row.formalSettlementNo
|
||||
"
|
||||
type="danger"
|
||||
@click="handleVoid(row)"
|
||||
>
|
||||
作废
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pre-settlement-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadTable"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<pre-settlement-editor
|
||||
v-model="editor.visible"
|
||||
:record-id="editor.id"
|
||||
:readonly="editor.readonly"
|
||||
@success="loadTable"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="advanceDialog.visible" title="预付申请" width="520px" append-to-body>
|
||||
<el-form
|
||||
ref="advanceFormRef"
|
||||
:model="advanceForm"
|
||||
:rules="advanceRules"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-form-item label="预结算单号">
|
||||
<span>{{ advanceDialog.row.preSettlementNo || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="结算金额">
|
||||
<span>
|
||||
{{ formatMoney(advanceDialog.row.settlementAmount, advanceDialog.row.currency) }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="已申请预付金额">
|
||||
<span>
|
||||
{{ formatMoney(advanceDialog.row.advanceAppliedAmount, advanceDialog.row.currency) }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请预付金额" prop="appliedAmount">
|
||||
<el-input-number
|
||||
v-model="advanceForm.appliedAmount"
|
||||
:min="0"
|
||||
:max="advanceAvailableAmount"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="金蝶预付单号">
|
||||
<el-input
|
||||
v-model="advanceForm.kingdeeAdvanceNo"
|
||||
maxlength="100"
|
||||
placeholder="外部系统回写时可填写"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="advanceDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="advanceDialog.submitting" @click="submitAdvance">
|
||||
提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="printDialog.visible" title="打印" width="620px" append-to-body>
|
||||
<div class="dialog-section-title">选择打印模板</div>
|
||||
<el-form label-position="right" label-width="auto" class="pre-settlement-page__print-form">
|
||||
<el-form-item label="项目">
|
||||
<span>{{ printDialog.row.projectName || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板名称">
|
||||
<el-select v-model="printDialog.template" placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in printDialog.templates"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="printDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="printDialog.loading" @click="handlePrintPreview">
|
||||
打印预览
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="flowDialog.visible" title="审批流程" width="620px" append-to-body>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="预结算单号">
|
||||
{{ flowDialog.row.preSettlementNo || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="审核状态">
|
||||
{{ flowDialog.row.approvalStatusName || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前节点">
|
||||
{{ flowDialog.row.currentNode || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前处理人">
|
||||
{{ flowDialog.row.currentProcessor || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import {
|
||||
applyAdvance,
|
||||
approve,
|
||||
exportList,
|
||||
formalSettlement,
|
||||
getDetail,
|
||||
getList,
|
||||
getPrintTemplates,
|
||||
remove,
|
||||
returnBill,
|
||||
voidBill,
|
||||
} from '@/api/settlement/preSettlement';
|
||||
import { preSettlementSearchFields } from '@/option/settlement/preSettlementSearch';
|
||||
import { preSettlementTableColumns } from '@/option/settlement/preSettlementTable';
|
||||
import { downloadFile } from '@/utils/util';
|
||||
import PreSettlementEditor from './components/pre-settlement-editor.vue';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
preSettlementNo: '',
|
||||
advanceNo: '',
|
||||
projectName: '',
|
||||
deptName: '',
|
||||
contractName: '',
|
||||
contractNo: '',
|
||||
payeeName: '',
|
||||
payerName: '',
|
||||
createDateRange: [],
|
||||
approvalStatus: '',
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'PreSettlement',
|
||||
components: { PreSettlementEditor },
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
loading: false,
|
||||
query: emptyQuery(),
|
||||
searchFields: preSettlementSearchFields,
|
||||
searchExpanded: false,
|
||||
tableColumns: preSettlementTableColumns,
|
||||
rows: [],
|
||||
selection: [],
|
||||
page: {
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0,
|
||||
},
|
||||
editor: {
|
||||
visible: false,
|
||||
id: '',
|
||||
readonly: false,
|
||||
},
|
||||
advanceDialog: {
|
||||
visible: false,
|
||||
submitting: false,
|
||||
row: {},
|
||||
},
|
||||
advanceForm: {
|
||||
appliedAmount: null,
|
||||
kingdeeAdvanceNo: '',
|
||||
},
|
||||
advanceRules: {
|
||||
appliedAmount: [
|
||||
{ required: true, message: '请输入申请预付金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (!value || Number(value) <= 0) {
|
||||
callback(new Error('申请预付金额必须大于0'));
|
||||
return;
|
||||
}
|
||||
if (Number(value) > this.advanceAvailableAmount) {
|
||||
callback(new Error('申请预付金额不能超过剩余可申请金额'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
},
|
||||
printDialog: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
row: {},
|
||||
templates: [],
|
||||
template: '',
|
||||
},
|
||||
flowDialog: {
|
||||
visible: false,
|
||||
row: {},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo?.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
},
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
advanceAvailableAmount() {
|
||||
return Math.max(
|
||||
Number(this.advanceDialog.row.settlementAmount || 0) -
|
||||
Number(this.advanceDialog.row.advanceAppliedAmount || 0),
|
||||
0
|
||||
);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadTable();
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||||
},
|
||||
async loadTable() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const { data } = await getList(this.page.current, this.page.size, this.buildQuery());
|
||||
const result = data?.data || {};
|
||||
this.rows = result.records || [];
|
||||
this.page.total = Number(result.total || 0);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
buildQuery() {
|
||||
const range = this.query.createDateRange || [];
|
||||
return {
|
||||
...this.query,
|
||||
createDateRange: undefined,
|
||||
createStartDate: range[0],
|
||||
createEndDate: range[1],
|
||||
};
|
||||
},
|
||||
handleSearch() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
resetSearch() {
|
||||
this.query = emptyQuery();
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
toggleSearch() {
|
||||
this.searchExpanded = !this.searchExpanded;
|
||||
},
|
||||
handleSizeChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
handleSelectionChange(rows) {
|
||||
this.selection = rows;
|
||||
},
|
||||
openCreate() {
|
||||
this.editor = { visible: true, id: '', readonly: false };
|
||||
},
|
||||
openEdit(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: false };
|
||||
},
|
||||
openView(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: true };
|
||||
},
|
||||
isEditable(row) {
|
||||
return ['draft', 'returned'].includes(row.approvalStatus);
|
||||
},
|
||||
async handleDelete(row) {
|
||||
await this.$confirm('确认删除该预结算草稿?', '提示', { type: 'warning' });
|
||||
await remove(row.id);
|
||||
this.$message.success('删除成功');
|
||||
this.loadTable();
|
||||
},
|
||||
selectedOne(operationName) {
|
||||
if (this.selection.length !== 1) {
|
||||
this.$message.warning(`${operationName}需选择一条预结算单`);
|
||||
return null;
|
||||
}
|
||||
return this.selection[0];
|
||||
},
|
||||
openAdvanceDialog() {
|
||||
const row = this.selectedOne('预付申请');
|
||||
if (!row) return;
|
||||
if (
|
||||
row.approvalStatus !== 'approved' ||
|
||||
row.settlementType !== 'payable' ||
|
||||
row.formalSettlementNo
|
||||
) {
|
||||
this.$message.warning('仅审批通过、未转正式结算的应付预结算单可发起预付');
|
||||
return;
|
||||
}
|
||||
this.advanceDialog.row = row;
|
||||
this.advanceDialog.visible = true;
|
||||
this.advanceForm = { appliedAmount: null, kingdeeAdvanceNo: '' };
|
||||
this.$nextTick(() => this.$refs.advanceFormRef?.clearValidate());
|
||||
},
|
||||
async submitAdvance() {
|
||||
await this.$refs.advanceFormRef?.validate();
|
||||
this.advanceDialog.submitting = true;
|
||||
try {
|
||||
await applyAdvance({
|
||||
preSettlementId: this.advanceDialog.row.id,
|
||||
appliedAmount: this.advanceForm.appliedAmount,
|
||||
kingdeeAdvanceNo: this.advanceForm.kingdeeAdvanceNo,
|
||||
});
|
||||
this.$message.success('预付申请提交成功');
|
||||
this.advanceDialog.visible = false;
|
||||
this.loadTable();
|
||||
} finally {
|
||||
this.advanceDialog.submitting = false;
|
||||
}
|
||||
},
|
||||
async handleFormalSettlement() {
|
||||
const row = this.selectedOne('尾款结算');
|
||||
if (!row) return;
|
||||
if (
|
||||
row.approvalStatus !== 'approved' ||
|
||||
row.settlementType !== 'payable' ||
|
||||
row.formalSettlementNo
|
||||
) {
|
||||
this.$message.warning('仅审批通过、未转正式结算的应付预结算单可发起尾款结算');
|
||||
return;
|
||||
}
|
||||
await this.$confirm('转正式结算后无法发起预付,确认继续?', '提示', {
|
||||
type: 'warning',
|
||||
});
|
||||
const { data } = await formalSettlement(row.id);
|
||||
this.$message.success(`正式结算单已生成:${data?.data || ''}`);
|
||||
this.loadTable();
|
||||
},
|
||||
async openPrintDialog() {
|
||||
const row = this.selectedOne('打印结算单');
|
||||
if (!row) return;
|
||||
if (row.approvalStatus === 'voided') {
|
||||
this.$message.warning('已作废的预结算单不能打印');
|
||||
return;
|
||||
}
|
||||
this.printDialog.row = row;
|
||||
this.printDialog.visible = true;
|
||||
this.printDialog.loading = true;
|
||||
try {
|
||||
const { data } = await getPrintTemplates(row.id);
|
||||
this.printDialog.templates = data?.data || [];
|
||||
this.printDialog.template = this.printDialog.templates[0]?.value || '';
|
||||
} finally {
|
||||
this.printDialog.loading = false;
|
||||
}
|
||||
},
|
||||
async handlePrintPreview() {
|
||||
if (!this.printDialog.template) {
|
||||
this.$message.warning('请选择打印模板');
|
||||
return;
|
||||
}
|
||||
const previewWindow = window.open('', '_blank');
|
||||
if (!previewWindow) {
|
||||
this.$message.warning('浏览器阻止了打印预览窗口,请允许弹出窗口后重试');
|
||||
return;
|
||||
}
|
||||
this.printDialog.loading = true;
|
||||
try {
|
||||
const { data } = await getDetail(this.printDialog.row.id);
|
||||
const detail = data?.data || {};
|
||||
previewWindow.document.open();
|
||||
previewWindow.document.write(this.buildPrintHtml(detail));
|
||||
previewWindow.document.close();
|
||||
this.printDialog.visible = false;
|
||||
} catch (error) {
|
||||
previewWindow.close();
|
||||
throw error;
|
||||
} finally {
|
||||
this.printDialog.loading = false;
|
||||
}
|
||||
},
|
||||
buildPrintHtml(detail) {
|
||||
const summaryRows = (detail.summaryFees || [])
|
||||
.map(
|
||||
(row, index) =>
|
||||
`<tr><td>${index + 1}</td><td>${this.escapeHtml(row.feeType)}</td><td>${this.escapeHtml(
|
||||
row.feeItem
|
||||
)}</td><td>${this.escapeHtml(
|
||||
this.formatMoney(row.originalAmount, detail.currency)
|
||||
)}</td><td>${this.escapeHtml(
|
||||
this.formatMoney(row.adjustAmount, detail.currency)
|
||||
)}</td><td>${this.escapeHtml(
|
||||
this.formatMoney(row.settlementAmount, detail.currency)
|
||||
)}</td></tr>`
|
||||
)
|
||||
.join('');
|
||||
return `<!doctype html><html><head><meta charset="utf-8"><title>${this.escapeHtml(
|
||||
detail.preSettlementNo || '预结算单'
|
||||
)}</title><style>body{font-family:Arial,"Microsoft YaHei",sans-serif;color:#222;padding:28px}h1{text-align:center;font-size:24px}.meta{display:grid;grid-template-columns:repeat(3,1fr);gap:14px 24px;margin:24px 0}.meta div{border-bottom:1px solid #ddd;padding:7px 0}table{width:100%;border-collapse:collapse;margin-top:16px}th,td{border:1px solid #bbb;padding:9px;text-align:center;font-size:13px}th{background:#f5f5f5}.actions{text-align:center;margin-top:24px}@media print{.actions{display:none}}</style></head><body><h1>预结算单</h1><div class="meta"><div>预结算单号:${this.escapeHtml(
|
||||
detail.preSettlementNo
|
||||
)}</div><div>项目名称:${this.escapeHtml(
|
||||
detail.projectName
|
||||
)}</div><div>合同编号:${this.escapeHtml(
|
||||
detail.contractNo
|
||||
)}</div><div>合同名称:${this.escapeHtml(
|
||||
detail.contractName
|
||||
)}</div><div>收款方:${this.escapeHtml(detail.payeeName)}</div><div>付款方:${this.escapeHtml(
|
||||
detail.payerName
|
||||
)}</div><div>结算类型:${
|
||||
detail.settlementType === 'receivable' ? '应收' : '应付'
|
||||
}</div><div>结算金额:${this.escapeHtml(
|
||||
this.formatMoney(detail.settlementAmount, detail.currency)
|
||||
)}</div><div>本位币合计:${this.escapeHtml(
|
||||
this.formatMoney(detail.localSettlementAmount, detail.localCurrency)
|
||||
)}</div><div>汇率日期:${this.escapeHtml(
|
||||
detail.exchangeRateDate
|
||||
)}</div><div>结算汇率:${this.escapeHtml(
|
||||
detail.exchangeRate
|
||||
)}</div><div>创建人:${this.escapeHtml(
|
||||
detail.createUserName
|
||||
)}</div></div><h2>结算合计</h2><table><thead><tr><th>序号</th><th>费用类型</th><th>费用项</th><th>原金额</th><th>调整金额</th><th>结算金额</th></tr></thead><tbody>${summaryRows}</tbody></table><div class="actions"><button onclick="window.print()">打印 / 另存为 PDF</button></div></body></html>`;
|
||||
},
|
||||
async handleExport() {
|
||||
const { data } = await exportList(this.buildQuery());
|
||||
downloadFile(data, `预结算单${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`);
|
||||
},
|
||||
openFlow(row) {
|
||||
this.flowDialog.row = row;
|
||||
this.flowDialog.visible = true;
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('确认审批通过该预结算单?', '提示', { type: 'warning' });
|
||||
await approve({ id: row.id });
|
||||
this.$message.success('审批通过');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleReturn(row) {
|
||||
const { value } = await this.$prompt('请输入驳回原因', '审批驳回', {
|
||||
inputType: 'textarea',
|
||||
inputValidator: value => Boolean(value?.trim()) || '请输入驳回原因',
|
||||
});
|
||||
await returnBill({ id: row.id, reason: value });
|
||||
this.$message.success('已驳回');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleVoid(row) {
|
||||
const { value } = await this.$prompt('请输入作废原因', '作废预结算单', {
|
||||
inputType: 'textarea',
|
||||
inputValidator: value => Boolean(value?.trim()) || '请输入作废原因',
|
||||
});
|
||||
await voidBill({ id: row.id, reason: value });
|
||||
this.$message.success('作废成功');
|
||||
this.loadTable();
|
||||
},
|
||||
formatMoney(value, currency = 'RMB') {
|
||||
if (value === undefined || value === null || value === '') return '-';
|
||||
const amount = Number(value);
|
||||
if (!Number.isFinite(amount)) return '-';
|
||||
return `${amount.toFixed(2)} ${currency || 'RMB'}`;
|
||||
},
|
||||
formatNumber(value, precision = 2) {
|
||||
if (value === undefined || value === null || value === '') return '-';
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number.toFixed(precision) : '-';
|
||||
},
|
||||
moneyCurrency(row, prop) {
|
||||
return prop === 'localSettlementAmount' ? row.localCurrency : row.currency;
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === undefined || value === null || value === '' ? '-' : value;
|
||||
},
|
||||
statusTagType(status) {
|
||||
return {
|
||||
draft: 'info',
|
||||
reviewing: 'warning',
|
||||
approved: 'success',
|
||||
returned: 'danger',
|
||||
voided: 'info',
|
||||
}[status];
|
||||
},
|
||||
escapeHtml(value) {
|
||||
return String(value ?? '-')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pre-settlement-page {
|
||||
&__search {
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
&__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px 24px;
|
||||
align-items: start;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-select),
|
||||
:deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__search-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__table-panel {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 56px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&__toolbar-left,
|
||||
&__toolbar-right,
|
||||
&__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&__print-form {
|
||||
margin-top: 20px;
|
||||
|
||||
:deep(.el-select) {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.pre-settlement-page__search-grid {
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 760px) {
|
||||
.pre-settlement-page__search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
&::before {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
background: #409eff;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.pre-settlement-page .el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
:deep(.pre-settlement-page .el-table__body tr:nth-child(even) > td.el-table__cell),
|
||||
:deep(.pre-settlement-page .el-table__body tr:nth-child(even) > td.el-table-fixed-column--left),
|
||||
:deep(.pre-settlement-page .el-table__body tr:nth-child(even) > td.el-table-fixed-column--right) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
:deep(.pre-settlement-page.basic-container .basic-container__card > .el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -21,7 +21,9 @@
|
||||
<div class="settlement-detail-page__search-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch" />
|
||||
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
|
||||
{{ searchExpanded ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -31,8 +33,8 @@
|
||||
<div class="settlement-detail-page__toolbar">
|
||||
<div class="settlement-detail-page__toolbar-left">
|
||||
<el-button type="primary" @click="openGenerateDialog">生成费用</el-button>
|
||||
<el-button type="primary" @click="openUpdateFeeDialog">更新费用</el-button>
|
||||
<el-button type="primary" @click="openTransferDialog">批量转结算</el-button>
|
||||
<el-button type="primary" plain @click="openUpdateFeeDialog">更新费用</el-button>
|
||||
<el-button type="primary" plain @click="openTransferDialog">批量转结算</el-button>
|
||||
<el-button type="primary" plain @click="handleExport">导出</el-button>
|
||||
</div>
|
||||
<div class="settlement-detail-page__toolbar-right">
|
||||
@@ -55,7 +57,7 @@
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in tableColumns"
|
||||
v-for="column in displayTableColumns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
@@ -71,16 +73,16 @@
|
||||
>
|
||||
{{ row[column.prop] }}
|
||||
</el-link>
|
||||
<span v-else>{{ row[column.prop] || '-' }}</span>
|
||||
<span v-else>{{ formatColumnValue(row, column) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="settlement-detail-page__actions">
|
||||
<el-link v-if="row.settlementStatus === 'pending'" type="primary" @click="openUpdateFeeDialog(row)">
|
||||
<el-link v-if="row.settlementStatus === 'pending'" type="primary" @click="openAdjustDialog(row)">
|
||||
调整
|
||||
</el-link>
|
||||
<el-link v-if="row.settlementStatus === 'pending'" type="primary" @click="closeRow(row)">
|
||||
<el-link v-if="row.settlementStatus === 'pending'" type="danger" @click="closeRow(row)">
|
||||
关闭
|
||||
</el-link>
|
||||
<el-link v-else type="primary" disabled>-</el-link>
|
||||
@@ -122,7 +124,7 @@
|
||||
:align="column.align || 'center'"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ formatCell(row[column.prop]) }}</template>
|
||||
<template #default="{ row }">{{ formatDetailCell(row, column.prop) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
@@ -154,6 +156,134 @@
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="adjustDialog.visible"
|
||||
title="调整费用"
|
||||
width="96%"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="settlement-detail-page__adjust-toolbar">
|
||||
<el-button type="primary" plain @click="addManualAdjustRow">新增费用项</el-button>
|
||||
</div>
|
||||
<el-table v-loading="adjustDialog.loading" :data="adjustRows" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column label="手工费用项目" min-width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="row.manualFee"
|
||||
v-model="row.feeItemName"
|
||||
clearable
|
||||
placeholder="请输入收费或扣费项目"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收费/扣费" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-if="row.manualFee"
|
||||
v-model="row.feeType"
|
||||
placeholder="请选择"
|
||||
@change="recalculateManualAdjustRow(row)"
|
||||
>
|
||||
<el-option label="收费" value="charge" />
|
||||
<el-option label="扣费" value="deduct" />
|
||||
</el-select>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="手工金额" width="150" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="row.manualFee"
|
||||
v-model="row.amount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateManualAdjustRow(row)"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="row.manualFee && !row.id"
|
||||
type="danger"
|
||||
@click="removeManualAdjustRow(row)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-for="column in adjustFeeColumns"
|
||||
:key="column.prop || column.feeItemName"
|
||||
:label="column.label"
|
||||
:min-width="column.minWidth"
|
||||
:align="column.align || 'center'"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="column.prop === 'transportQuantityText' && !row.manualFee"
|
||||
v-model="row.transportQuantity"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateAdjustRow(row)"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="column.prop === 'mileage' && !row.manualFee"
|
||||
v-model="row.mileage"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateAdjustRow(row)"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="column.prop === 'freightAmount' && !row.manualFee"
|
||||
v-model="row.freightAmount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateAdjustRow(row, 'freight')"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="column.dynamic && !row.manualFee"
|
||||
v-model="row.feeItems[column.feeItemName]"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateAdjustRow(row, column.feeItemName)"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="column.prop === 'remark'"
|
||||
v-model="row.remark"
|
||||
clearable
|
||||
maxlength="200"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
<span v-else-if="column.prop === 'adjustAmountText'">
|
||||
{{ fixedTwoDecimals(row.adjustAmount) }}
|
||||
</span>
|
||||
<span v-else-if="column.prop === 'afterAmountText'">
|
||||
{{ fixedTwoDecimals(row.afterAmount) }}
|
||||
</span>
|
||||
<span v-else>{{ formatDetailCell(row, column.prop) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="adjustDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="adjustDialog.submitting" @click="saveAdjustFee">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="updateFeeDialog.visible" title="更新费用" width="620px" append-to-body>
|
||||
<el-form
|
||||
ref="updateFeeFormRef"
|
||||
@@ -169,9 +299,10 @@
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择合同"
|
||||
@change="handleUpdateContractChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
v-for="item in updateContractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:value="item.id"
|
||||
@@ -179,7 +310,13 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同计费方案" prop="billingPlanId">
|
||||
<el-select v-model="updateFeeForm.billingPlanId" clearable filterable placeholder="请选择">
|
||||
<el-select
|
||||
v-model="updateFeeForm.billingPlanId"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="!updateFeeForm.contractId"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in billingPlanOptions"
|
||||
:key="item.id"
|
||||
@@ -190,10 +327,10 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="primary" :loading="updateFeeDialog.submitting" @click="confirmUpdateFee">
|
||||
确认
|
||||
</el-button>
|
||||
<el-button @click="updateFeeDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="updateFeeDialog.submitting" @click="confirmUpdateFee">
|
||||
提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -201,7 +338,10 @@
|
||||
<div class="settlement-detail-page__transfer-form">
|
||||
<el-form :model="transferQuery" inline label-position="right" label-width="120px">
|
||||
<el-form-item label="转结算类型">
|
||||
<el-radio-group v-model="transferForm.settlementBillType">
|
||||
<el-radio-group
|
||||
v-model="transferForm.settlementBillType"
|
||||
@change="loadTransferCandidates"
|
||||
>
|
||||
<el-radio label="pre">预结算单</el-radio>
|
||||
<el-radio label="formal">正式结算单</el-radio>
|
||||
</el-radio-group>
|
||||
@@ -246,13 +386,15 @@
|
||||
:min-width="column.minWidth"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">{{ formatColumnValue(row, column) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button type="primary" :loading="transferDialog.submitting" @click="submitTransfer">
|
||||
确认
|
||||
</el-button>
|
||||
<el-button @click="transferDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="transferDialog.submitting" @click="submitTransfer">
|
||||
提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -323,7 +465,7 @@
|
||||
<el-link v-if="column.link && row[column.prop]" type="primary">
|
||||
{{ row[column.prop] }}
|
||||
</el-link>
|
||||
<span v-else>{{ row[column.prop] || '-' }}</span>
|
||||
<span v-else>{{ formatColumnValue(row, column) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -339,8 +481,8 @@
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="openGeneratePreview">生成费用</el-button>
|
||||
<el-button @click="generateDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="openGeneratePreview">生成费用</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -369,11 +511,11 @@
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="previewDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="generateDialog.visible = true">上一步</el-button>
|
||||
<el-button type="primary" :loading="previewDialog.submitting" @click="submitGenerateFee">
|
||||
确认
|
||||
提交
|
||||
</el-button>
|
||||
<el-button @click="previewDialog.visible = false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
@@ -395,6 +537,7 @@ import {
|
||||
import * as api from '@/api/settlement/receivable-payable-detail';
|
||||
import { getList as getContractList } from '@/api/business/contract-manage';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
|
||||
export default {
|
||||
@@ -412,6 +555,7 @@ export default {
|
||||
Setting,
|
||||
searchFields,
|
||||
tableColumns,
|
||||
tableFeeItemNames: [],
|
||||
changeRecordColumns,
|
||||
transferSearchFields,
|
||||
generateWaybillColumns,
|
||||
@@ -424,6 +568,9 @@ export default {
|
||||
detailDialog: { visible: false, title: '费用明细', activeTab: 'fee', row: null, loading: false },
|
||||
feeRows: [],
|
||||
dynamicFeeColumns: [],
|
||||
adjustDialog: { visible: false, loading: false, submitting: false, row: null },
|
||||
adjustRows: [],
|
||||
adjustDynamicFeeColumns: [],
|
||||
changeRows: [],
|
||||
changePage: { current: 1, size: 10, total: 0 },
|
||||
changeDialog: { loading: false },
|
||||
@@ -433,6 +580,8 @@ export default {
|
||||
contractId: [{ required: true, message: '请选择更新范围', trigger: 'change' }],
|
||||
},
|
||||
contractOptions: [],
|
||||
updateContractOptions: [],
|
||||
transportTypeOptions: [],
|
||||
billingPlanOptions: [],
|
||||
transferDialog: { visible: false, loading: false, submitting: false },
|
||||
transferQuery: {},
|
||||
@@ -456,22 +605,67 @@ export default {
|
||||
return '应收应付';
|
||||
},
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 7);
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
feeDetailColumns() {
|
||||
return [...feeDetailBaseColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
|
||||
},
|
||||
displayTableColumns() {
|
||||
const columns = [...this.tableColumns];
|
||||
const totalIndex = columns.findIndex(column => column.prop === 'totalAmountText');
|
||||
const dynamicColumns = this.tableFeeItemNames.map((name, index) => ({
|
||||
label: name,
|
||||
prop: `tableFeeItem${index}`,
|
||||
feeItemName: name,
|
||||
dynamic: true,
|
||||
minWidth: 130,
|
||||
align: 'right',
|
||||
}));
|
||||
if (totalIndex < 0) return [...columns, ...dynamicColumns];
|
||||
columns.splice(totalIndex, 0, ...dynamicColumns);
|
||||
return columns;
|
||||
},
|
||||
previewColumns() {
|
||||
return [...generatePreviewColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
|
||||
},
|
||||
adjustFeeColumns() {
|
||||
return [
|
||||
...feeDetailBaseColumns,
|
||||
...this.adjustDynamicFeeColumns,
|
||||
...feeDetailTailColumns,
|
||||
];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadTransportTypeOptions();
|
||||
this.loadContracts();
|
||||
this.loadTable();
|
||||
},
|
||||
methods: {
|
||||
async loadTransportTypeOptions() {
|
||||
const res = await getDictionary({ code: 'transport_type' });
|
||||
const records = res.data?.data || [];
|
||||
this.transportTypeOptions = records.map(item => ({
|
||||
label: item.dictValue || item.label || item.name,
|
||||
value: item.dictKey || item.value || item.dictValue || item.name,
|
||||
}));
|
||||
},
|
||||
transportTypeLabel(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
return (
|
||||
this.transportTypeOptions.find(item => String(item.value) === String(value))?.label || value
|
||||
);
|
||||
},
|
||||
formatColumnValue(row, column) {
|
||||
if (column.prop === 'transportType') return this.transportTypeLabel(row[column.prop]);
|
||||
if (column.dynamic) {
|
||||
return this.money(this.normalizeFeeItems(row.feeItems)[column.feeItemName], row.currency || 'RMB');
|
||||
}
|
||||
return this.formatDetailCell(row, column.prop);
|
||||
},
|
||||
async loadTable() {
|
||||
this.loading = true;
|
||||
this.tableFeeItemNames = [];
|
||||
try {
|
||||
const params = this.buildRequestParams(
|
||||
this.normalizeQuery(this.query, 'generateDateRange', 'generateStartDate', 'generateEndDate')
|
||||
@@ -479,6 +673,7 @@ export default {
|
||||
const res = await api.getList(this.page.current, this.page.size, params);
|
||||
const data = this.unwrapPage(res);
|
||||
this.rows = (data.records || []).map(this.decorateRow);
|
||||
this.tableFeeItemNames = this.collectFeeItemNames(this.rows);
|
||||
this.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
@@ -511,6 +706,132 @@ export default {
|
||||
this.detailDialog = { ...this.detailDialog, visible: true, row, title: row.documentNo, activeTab: 'fee' };
|
||||
await this.loadFeeDetail();
|
||||
},
|
||||
async openAdjustDialog(row) {
|
||||
this.adjustDialog = { ...this.adjustDialog, visible: true, row, loading: true };
|
||||
try {
|
||||
const res = await api.getFeeDetail(row.id);
|
||||
const data = res.data?.data || res.data || res || {};
|
||||
this.adjustDynamicFeeColumns = (data.feeItemNames || []).map(name => ({
|
||||
label: name,
|
||||
feeItemName: name,
|
||||
dynamic: true,
|
||||
minWidth: 150,
|
||||
align: 'right',
|
||||
}));
|
||||
this.adjustRows = (data.records || []).map(item => {
|
||||
const feeItems = {};
|
||||
(data.feeItemNames || []).forEach(name => {
|
||||
feeItems[name] = Number(item.feeItems?.[name] || 0);
|
||||
});
|
||||
const adjusted = {
|
||||
...item,
|
||||
transportQuantity: Number(item.transportQuantity || 0),
|
||||
mileage:
|
||||
item.mileage === null || item.mileage === undefined || Number(item.mileage) === -1
|
||||
? null
|
||||
: Number(item.mileage),
|
||||
freightAmount: Number(item.freightAmount || 0),
|
||||
originalAmount: Number(item.originalAmount || 0),
|
||||
feeItems,
|
||||
manualFee: item.billingFactor === '手工调整',
|
||||
feeItemName:
|
||||
item.billingFactor === '手工调整' ? Object.keys(feeItems)[0] || '' : '',
|
||||
feeType: item.billingType === '手工扣费' ? 'deduct' : 'charge',
|
||||
amount:
|
||||
item.billingFactor === '手工调整'
|
||||
? Math.abs(Number(item.afterAmount || 0))
|
||||
: 0,
|
||||
};
|
||||
if (adjusted.manualFee) this.recalculateManualAdjustRow(adjusted);
|
||||
else this.recalculateAdjustRow(adjusted);
|
||||
return adjusted;
|
||||
});
|
||||
} finally {
|
||||
this.adjustDialog.loading = false;
|
||||
}
|
||||
},
|
||||
addManualAdjustRow() {
|
||||
this.adjustRows.push({
|
||||
id: null,
|
||||
manualFee: true,
|
||||
feeItemName: '',
|
||||
feeType: 'charge',
|
||||
amount: 0,
|
||||
transportQuantity: null,
|
||||
mileage: null,
|
||||
freightAmount: 0,
|
||||
originalAmount: 0,
|
||||
feeItems: {},
|
||||
remark: '',
|
||||
});
|
||||
},
|
||||
removeManualAdjustRow(row) {
|
||||
const index = this.adjustRows.indexOf(row);
|
||||
if (index >= 0) this.adjustRows.splice(index, 1);
|
||||
},
|
||||
recalculateManualAdjustRow(row) {
|
||||
const amount = Number(row.amount || 0);
|
||||
row.afterAmount = Number((row.feeType === 'deduct' ? -amount : amount).toFixed(2));
|
||||
row.adjustAmount = Number((row.afterAmount - Number(row.originalAmount || 0)).toFixed(2));
|
||||
},
|
||||
fixedTwoDecimals(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
recalculateAdjustRow(row, changedField) {
|
||||
if (changedField && changedField !== 'freight' && this.isFreightFeeItem(changedField)) {
|
||||
row.freightAmount = Number(row.feeItems[changedField] || 0);
|
||||
}
|
||||
if (changedField === 'freight') {
|
||||
const freightItem = Object.keys(row.feeItems).find(this.isFreightFeeItem);
|
||||
if (freightItem) row.feeItems[freightItem] = Number(row.freightAmount || 0);
|
||||
}
|
||||
const feeItemTotal = Object.values(row.feeItems).reduce(
|
||||
(total, value) => total + Number(value || 0),
|
||||
0
|
||||
);
|
||||
const hasFreightItem = Object.keys(row.feeItems).some(this.isFreightFeeItem);
|
||||
row.afterAmount = Number((hasFreightItem ? feeItemTotal : Number(row.freightAmount || 0) + feeItemTotal).toFixed(2));
|
||||
row.adjustAmount = Number((row.afterAmount - Number(row.originalAmount || 0)).toFixed(2));
|
||||
},
|
||||
isFreightFeeItem(name) {
|
||||
return String(name || '').includes('运费') || String(name || '').includes('运输费');
|
||||
},
|
||||
async saveAdjustFee() {
|
||||
if (!this.adjustRows.length || !this.adjustDialog.row) {
|
||||
this.$message.warning('没有可调整的费用明细');
|
||||
return;
|
||||
}
|
||||
const invalidManualRow = this.adjustRows.find(
|
||||
row => row.manualFee && (!String(row.feeItemName || '').trim() || Number(row.amount || 0) <= 0)
|
||||
);
|
||||
if (invalidManualRow) {
|
||||
this.$message.warning('请完整填写手工费用项目和金额');
|
||||
return;
|
||||
}
|
||||
this.adjustDialog.submitting = true;
|
||||
try {
|
||||
await api.adjustFee({
|
||||
detailId: this.adjustDialog.row.id,
|
||||
rows: this.adjustRows.map(row => ({
|
||||
id: row.id,
|
||||
transportQuantity: row.transportQuantity,
|
||||
mileage: row.mileage,
|
||||
freightAmount: row.freightAmount,
|
||||
feeItems: row.feeItems,
|
||||
manualFee: row.manualFee === true,
|
||||
feeItemName: row.feeItemName,
|
||||
feeType: row.feeType,
|
||||
amount: row.amount,
|
||||
remark: row.remark,
|
||||
})),
|
||||
});
|
||||
this.$message.success('保存成功');
|
||||
this.adjustDialog.visible = false;
|
||||
await this.loadTable();
|
||||
} finally {
|
||||
this.adjustDialog.submitting = false;
|
||||
}
|
||||
},
|
||||
async loadFeeDetail() {
|
||||
if (!this.detailDialog.row) return;
|
||||
this.detailDialog.loading = true;
|
||||
@@ -555,20 +876,36 @@ export default {
|
||||
this.changePage.current = 1;
|
||||
this.loadChangeRecords();
|
||||
},
|
||||
openUpdateFeeDialog(row) {
|
||||
async openUpdateFeeDialog(row) {
|
||||
await this.loadUpdateFeeContracts();
|
||||
this.updateFeeDialog = { ...this.updateFeeDialog, visible: true, row: row || null };
|
||||
this.updateFeeForm = {
|
||||
ids: row ? [row.id] : this.selection.map(item => item.id),
|
||||
settlementType: this.settlementType || undefined,
|
||||
contractId: row?.contractId || '',
|
||||
billingPlanId: '',
|
||||
};
|
||||
this.syncBillingPlanOptions(this.updateFeeForm.contractId);
|
||||
this.handleUpdateContractChange(this.updateFeeForm.contractId);
|
||||
},
|
||||
async loadUpdateFeeContracts() {
|
||||
const res = await api.getUpdateFeeContracts({
|
||||
settlementType: this.settlementType || undefined,
|
||||
});
|
||||
this.updateContractOptions = res.data?.data || [];
|
||||
},
|
||||
handleUpdateContractChange(contractId) {
|
||||
this.updateFeeForm.billingPlanId = '';
|
||||
if (!contractId) {
|
||||
this.billingPlanOptions = [];
|
||||
return;
|
||||
}
|
||||
const options = this.syncBillingPlanOptions(contractId);
|
||||
const selected = options.find(item => item.defaultPlan) || options[0];
|
||||
this.updateFeeForm.billingPlanId = selected?.id || '';
|
||||
},
|
||||
async confirmUpdateFee() {
|
||||
await this.$refs.updateFeeFormRef.validate();
|
||||
await this.$confirm(
|
||||
'更新费用将使用最新的合同计费规则重新生成费用明细,确认更新费用?',
|
||||
'将使用选定计费方案,更新该合同下所有待结算明细的费用,确认继续?',
|
||||
'确认提示',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
@@ -602,20 +939,73 @@ export default {
|
||||
},
|
||||
async loadTransferCandidates() {
|
||||
this.transferDialog.loading = true;
|
||||
this.transferSelection = [];
|
||||
try {
|
||||
const params = this.buildRequestParams(
|
||||
this.normalizeQuery(this.transferQuery, 'generateDateRange', 'generateStartDate', 'generateEndDate')
|
||||
this.normalizeQuery(
|
||||
this.transferQuery,
|
||||
'generateDateRange',
|
||||
'generateStartDate',
|
||||
'generateEndDate'
|
||||
)
|
||||
);
|
||||
const res = await api.getTransferCandidates(1, 50, {
|
||||
...params,
|
||||
settlementStatus: 'pending',
|
||||
settlementType: this.settlementType || undefined,
|
||||
settlementBillType: this.transferForm.settlementBillType,
|
||||
});
|
||||
const data = this.unwrapPage(res);
|
||||
this.transferRows = (data.records || []).map(this.decorateRow);
|
||||
this.transferRows = (data.records || [])
|
||||
.filter(row => this.isTransferCandidate(row))
|
||||
.map(this.decorateRow);
|
||||
} finally {
|
||||
this.transferDialog.loading = false;
|
||||
}
|
||||
},
|
||||
isTransferCandidate(row) {
|
||||
const hasValue = value => {
|
||||
if (Array.isArray(value)) return value.length > 0;
|
||||
return value !== null && value !== undefined && String(value).trim() !== '';
|
||||
};
|
||||
const normalizeSettlementType = value => {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === '应收') return 'receivable';
|
||||
if (normalized === '应付') return 'payable';
|
||||
return normalized;
|
||||
};
|
||||
const rowSettlementType = normalizeSettlementType(
|
||||
row.settlementType || row.settlementTypeName
|
||||
);
|
||||
const expectedSettlementType = normalizeSettlementType(this.settlementType);
|
||||
const matchesSettlementType =
|
||||
!expectedSettlementType || rowSettlementType === expectedSettlementType;
|
||||
const status = String(row.settlementStatus || '').trim().toLowerCase();
|
||||
const isClosed =
|
||||
[row.closed, row.isClosed, row.closeFlag, row.closedFlag].some(
|
||||
value => value === true || String(value).trim().toLowerCase() === 'true'
|
||||
) ||
|
||||
['closed', 'close', '已关闭'].includes(status);
|
||||
const hasPreSettlement = [
|
||||
row.preSettlementId,
|
||||
row.preSettlementIds,
|
||||
row.preSettlementNo,
|
||||
row.preSettlementNos,
|
||||
].some(hasValue);
|
||||
const hasFormalSettlement = [
|
||||
row.formalSettlementId,
|
||||
row.formalSettlementIds,
|
||||
row.formalSettlementNo,
|
||||
row.formalSettlementNos,
|
||||
].some(hasValue);
|
||||
return (
|
||||
matchesSettlementType &&
|
||||
['pending', '待结算'].includes(status) &&
|
||||
!isClosed &&
|
||||
!hasPreSettlement &&
|
||||
!hasFormalSettlement
|
||||
);
|
||||
},
|
||||
confirmTransferSelection() {
|
||||
if (!this.transferSelection.length) {
|
||||
this.$message.warning('请选择需要转结算的明细');
|
||||
@@ -763,14 +1153,16 @@ export default {
|
||||
return this.settlementType ? { ...params, settlementType: this.settlementType } : params;
|
||||
},
|
||||
syncBillingPlanOptions(contractId) {
|
||||
const contract = this.contractOptions.find(item => String(item.id) === String(contractId));
|
||||
const contracts = [...this.updateContractOptions, ...this.contractOptions];
|
||||
const contract = contracts.find(item => String(item.id) === String(contractId));
|
||||
const plans = this.parseJson(contract?.billingPlanJson);
|
||||
this.billingPlanOptions = plans.length
|
||||
? plans.map((item, index) => ({
|
||||
this.billingPlanOptions = plans.map((item, index) => ({
|
||||
id: item.id || item.planId || item.name || `plan-${index}`,
|
||||
name: item.name || item.planName || item.billingPlanName || `计费方案${index + 1}`,
|
||||
}))
|
||||
: [{ id: 'default', name: '默认计费方案' }];
|
||||
defaultPlan:
|
||||
item.defaultPlan === true || String(item.defaultPlan).toLowerCase() === 'true',
|
||||
}));
|
||||
return this.billingPlanOptions;
|
||||
},
|
||||
normalizeQuery(source, rangeProp, startProp, endProp) {
|
||||
const params = { ...source };
|
||||
@@ -796,10 +1188,41 @@ export default {
|
||||
'-',
|
||||
};
|
||||
},
|
||||
collectFeeItemNames(rows) {
|
||||
const names = [];
|
||||
(rows || []).forEach(row => {
|
||||
Object.keys(this.normalizeFeeItems(row.feeItems)).forEach(name => {
|
||||
if (!names.includes(name)) names.push(name);
|
||||
});
|
||||
});
|
||||
return names;
|
||||
},
|
||||
normalizeFeeItems(value) {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
||||
if (typeof value !== 'string' || !value.trim()) return {};
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
money(value, currency) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
return `${Number(value).toFixed(2)} ${currency}`;
|
||||
},
|
||||
formatDetailCell(row, prop) {
|
||||
if (
|
||||
prop === 'mileage' &&
|
||||
(row?.[prop] === null ||
|
||||
row?.[prop] === undefined ||
|
||||
row?.[prop] === '' ||
|
||||
Number(row[prop]) === -1)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
return this.formatCell(row?.[prop]);
|
||||
},
|
||||
formatCell(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
@@ -840,7 +1263,7 @@ export default {
|
||||
|
||||
.settlement-detail-page__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||
gap: 8px 24px;
|
||||
align-items: start;
|
||||
|
||||
@@ -863,10 +1286,6 @@ export default {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.settlement-detail-page__table-panel {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.settlement-detail-page__toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -885,6 +1304,7 @@ export default {
|
||||
.settlement-detail-page__table,
|
||||
.settlement-detail-page :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
background: #fff;
|
||||
|
||||
:deep(th.el-table__cell) {
|
||||
background: #f5f7fa;
|
||||
@@ -924,13 +1344,13 @@ export default {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1600px) {
|
||||
@media screen and (max-width: 1200px) {
|
||||
.settlement-detail-page__search-grid {
|
||||
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
@media screen and (max-width: 760px) {
|
||||
.settlement-detail-page__search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<basic-container class="settlement-adjustment-page">
|
||||
<section class="settlement-adjustment-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
|
||||
<div class="settlement-adjustment-page__search-grid">
|
||||
<el-form-item v-for="field in visibleSearchFields" :key="field.prop" :label="field.label">
|
||||
<el-date-picker
|
||||
v-if="field.type === 'daterange'"
|
||||
v-model="query[field.prop]"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD"
|
||||
range-separator="~"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'select'"
|
||||
v-model="query[field.prop]"
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
><el-option
|
||||
v-for="item in field.options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select>
|
||||
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<div class="settlement-adjustment-page__search-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
|
||||
{{ searchExpanded ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="settlement-adjustment-page__table-panel">
|
||||
<div class="settlement-adjustment-page__toolbar">
|
||||
<el-button
|
||||
v-if="hasPermission('settlement_adjustment_add')"
|
||||
type="primary"
|
||||
@click="openCreate"
|
||||
>新增</el-button
|
||||
><div class="settlement-adjustment-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border @selection-change="selection = $event">
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" /><el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="64"
|
||||
fixed="left"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-for="column in tableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
><template #default="{ row }"
|
||||
><el-link
|
||||
v-if="column.link && row[column.prop]"
|
||||
type="primary"
|
||||
@click="openView(row)"
|
||||
>{{ row[column.prop] }}</el-link
|
||||
><el-tag v-else-if="column.status" :type="statusType(row.approvalStatus)">{{
|
||||
row[column.prop] || '-'
|
||||
}}</el-tag
|
||||
><span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span
|
||||
><span v-else>{{ displayValue(row[column.prop]) }}</span></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column label="操作" width="320" fixed="right" align="center"
|
||||
><template #default="{ row }"
|
||||
><div class="settlement-adjustment-page__links">
|
||||
<el-link
|
||||
v-if="hasPermission('settlement_adjustment_view')"
|
||||
type="primary"
|
||||
@click="openView(row)"
|
||||
>查看</el-link
|
||||
><el-link
|
||||
v-if="hasPermission('settlement_adjustment_edit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="openEdit(row)"
|
||||
>编辑</el-link
|
||||
><el-link
|
||||
v-if="
|
||||
hasPermission('settlement_adjustment_delete') && row.approvalStatus === 'draft'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-link
|
||||
><el-link
|
||||
v-if="hasPermission('settlement_adjustment_submit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="handleSubmit(row)"
|
||||
>提交</el-link
|
||||
><el-link
|
||||
v-if="
|
||||
hasPermission('settlement_adjustment_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleApprove(row)"
|
||||
>通过</el-link
|
||||
><el-link
|
||||
v-if="
|
||||
hasPermission('settlement_adjustment_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleReturn(row)"
|
||||
>驳回</el-link
|
||||
><el-link
|
||||
v-if="
|
||||
hasPermission('settlement_adjustment_repush') &&
|
||||
row.approvalStatus === 'approved' &&
|
||||
row.kingdeeSyncStatus === 'synced'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleRepush(row)"
|
||||
>重新推送(高危)</el-link
|
||||
>
|
||||
</div></template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<div class="settlement-adjustment-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadTable"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<settlement-adjustment-editor
|
||||
v-model="editor.visible"
|
||||
:record-id="editor.id"
|
||||
:readonly="editor.readonly"
|
||||
@success="loadTable"
|
||||
/>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/settlement/settlementAdjustment';
|
||||
import { settlementAdjustmentSearchFields } from '@/option/settlement/settlementAdjustmentSearch';
|
||||
import { settlementAdjustmentTableColumns } from '@/option/settlement/settlementAdjustmentTable';
|
||||
import SettlementAdjustmentEditor from './components/settlement-adjustment-editor.vue';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
adjustmentNo: '',
|
||||
createDateRange: [],
|
||||
customerName: '',
|
||||
projectName: '',
|
||||
deptName: '',
|
||||
formalSettlementNo: '',
|
||||
settlementType: '',
|
||||
approvalStatus: '',
|
||||
});
|
||||
export default {
|
||||
name: 'SettlementAdjustment',
|
||||
components: { SettlementAdjustmentEditor },
|
||||
data: () => ({
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
loading: false,
|
||||
query: emptyQuery(),
|
||||
searchFields: settlementAdjustmentSearchFields,
|
||||
searchExpanded: false,
|
||||
tableColumns: settlementAdjustmentTableColumns,
|
||||
rows: [],
|
||||
selection: [],
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
editor: { visible: false, id: '', readonly: false },
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
isAdmin() {
|
||||
return String(this.userInfo?.authority || '').includes('admin');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadTable();
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||||
},
|
||||
async loadTable() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const range = this.query.createDateRange || [];
|
||||
const { data } = await api.getList(this.page.current, this.page.size, {
|
||||
...this.query,
|
||||
createDateRange: undefined,
|
||||
createStartDate: range[0],
|
||||
createEndDate: range[1],
|
||||
});
|
||||
const result = data?.data || {};
|
||||
this.rows = result.records || [];
|
||||
this.page.total = Number(result.total || 0);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
resetSearch() {
|
||||
this.query = emptyQuery();
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
toggleSearch() {
|
||||
this.searchExpanded = !this.searchExpanded;
|
||||
},
|
||||
handleSizeChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
openCreate() {
|
||||
this.editor = { visible: true, id: '', readonly: false };
|
||||
},
|
||||
openEdit(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: false };
|
||||
},
|
||||
openView(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: true };
|
||||
},
|
||||
isEditable(row) {
|
||||
return ['draft', 'returned'].includes(row.approvalStatus);
|
||||
},
|
||||
async handleDelete(row) {
|
||||
await this.$confirm('确认删除该结算调整草稿?', '提示', { type: 'warning' });
|
||||
await api.remove(row.id);
|
||||
this.$message.success('删除成功');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleSubmit(row) {
|
||||
await this.$confirm('确认提交审批?', '提示');
|
||||
await api.submit(row.id);
|
||||
this.$message.success('提交成功');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('审批通过后将更新关联正式结算单,确认继续?', '提示');
|
||||
await api.approve(row.id);
|
||||
if (row.kingdeeSyncStatus === 'synced')
|
||||
this.$message.warning(
|
||||
'调整已生效,本地已更新;原结算单已推送金蝶,请财务先手工冲销旧应付单后再重新推送。'
|
||||
);
|
||||
else this.$message.success('审批通过');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleReturn(row) {
|
||||
const { value } = await this.$prompt('请输入驳回原因', '驳回', {
|
||||
inputType: 'textarea',
|
||||
inputValidator: v => (v && v.length <= 200) || '请输入200字以内原因',
|
||||
});
|
||||
await api.returnBill(row.id, value);
|
||||
this.$message.success('已驳回');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleRepush(row) {
|
||||
await this.$confirm(
|
||||
'请确认金蝶旧应付单已由财务手工冲销,继续将生成全新财务应付单。',
|
||||
'高危操作确认',
|
||||
{ type: 'warning', confirmButtonText: '确认重新推送' }
|
||||
);
|
||||
const { data } = await api.repush(row.id);
|
||||
this.$message.success(`已生成新金蝶应付单:${data?.data || data || '-'}`);
|
||||
this.loadTable();
|
||||
},
|
||||
statusType(status) {
|
||||
return (
|
||||
{ approved: 'success', returned: 'danger', reviewing: 'warning', draft: 'info' }[status] ||
|
||||
'info'
|
||||
);
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
formatMoney(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settlement-adjustment-page {
|
||||
&__search {
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
&__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px 24px;
|
||||
align-items: start;
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
:deep(.el-input),
|
||||
:deep(.el-select),
|
||||
:deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&__search-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
&__table-panel {
|
||||
margin-top: 8px;
|
||||
}
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
padding: 12px;
|
||||
}
|
||||
&__toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
&__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
&__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1200px) {
|
||||
.settlement-adjustment-page__search-grid {
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 760px) {
|
||||
.settlement-adjustment-page__search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
:deep(.settlement-adjustment-page .el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
background: #fff;
|
||||
}
|
||||
:deep(.settlement-adjustment-page .el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<basic-container class="reconciliation-page">
|
||||
<section class="reconciliation-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
|
||||
<div class="reconciliation-page__search-grid">
|
||||
<el-form-item v-for="field in visibleSearchFields" :key="field.prop" :label="field.label">
|
||||
<el-select
|
||||
v-if="field.type === 'select'"
|
||||
v-model="query[field.prop]"
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in field.options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="reconciliation-page__search-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
|
||||
{{ searchExpanded ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="reconciliation-page__table-panel">
|
||||
<el-tabs v-model="settlementType" @tab-change="handleTabChange">
|
||||
<el-tab-pane label="应付" name="payable" />
|
||||
<el-tab-pane label="应收" name="receivable" />
|
||||
</el-tabs>
|
||||
<div class="reconciliation-page__toolbar">
|
||||
<div>
|
||||
<el-button
|
||||
v-if="hasPermission('transport_reconciliation_add')"
|
||||
type="primary"
|
||||
@click="openCreate"
|
||||
>新增</el-button
|
||||
>
|
||||
<el-button type="primary" plain @click="handleExport">导出</el-button>
|
||||
</div>
|
||||
<div class="reconciliation-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border @selection-change="selection = $event">
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="column.link" type="primary" @click="openView(row)">{{
|
||||
row[column.prop] || '-'
|
||||
}}</el-link>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop], row.currency) }}</span>
|
||||
<el-tag
|
||||
v-else-if="column.prop === 'reconciliationStatusName'"
|
||||
:type="row.reconciliationStatus === 'completed' ? 'success' : 'warning'"
|
||||
>{{ row[column.prop] || '-' }}</el-tag
|
||||
>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="reconciliation-page__links">
|
||||
<el-link type="primary" @click="openView(row)">查看</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('transport_reconciliation_edit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="openEdit(row)"
|
||||
>编辑</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('transport_reconciliation_delete') && isEditable(row)"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('transport_reconciliation_complete') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="handleComplete(row)"
|
||||
>确认</el-link
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="reconciliation-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadTable"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<transport-reconciliation-editor
|
||||
v-model="editor.visible"
|
||||
:record-id="editor.id"
|
||||
:settlement-type="settlementType"
|
||||
:readonly="editor.readonly"
|
||||
@success="loadTable"
|
||||
/>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as XLSX from 'xlsx';
|
||||
import * as api from '@/api/settlement/transportReconciliation';
|
||||
import { transportReconciliationSearchFields } from '@/option/settlement/transportReconciliationSearch';
|
||||
import { transportReconciliationTableColumns } from '@/option/settlement/transportReconciliationTable';
|
||||
import TransportReconciliationEditor from './components/transport-reconciliation-editor.vue';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
reconciliationNo: '',
|
||||
preSettlementNos: '',
|
||||
projectName: '',
|
||||
deptName: '',
|
||||
contractNo: '',
|
||||
payerName: '',
|
||||
payeeName: '',
|
||||
matchStatus: '',
|
||||
reconciliationStatus: '',
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'TransportReconciliation',
|
||||
components: { TransportReconciliationEditor },
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Refresh,
|
||||
query: emptyQuery(),
|
||||
searchExpanded: false,
|
||||
searchFields: transportReconciliationSearchFields,
|
||||
columns: transportReconciliationTableColumns,
|
||||
settlementType: 'payable',
|
||||
loading: false,
|
||||
rows: [],
|
||||
selection: [],
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
editor: { visible: false, id: null, readonly: false },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadTable();
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
return this.permission?.[code] !== false;
|
||||
},
|
||||
async loadTable() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const { data } = await api.getList(this.page.current, this.page.size, {
|
||||
...this.query,
|
||||
settlementType: this.settlementType,
|
||||
});
|
||||
this.rows = data.records || [];
|
||||
this.page.total = data.total || 0;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
resetSearch() {
|
||||
this.query = emptyQuery();
|
||||
this.handleSearch();
|
||||
},
|
||||
toggleSearch() {
|
||||
this.searchExpanded = !this.searchExpanded;
|
||||
},
|
||||
handleTabChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
handleSizeChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
openCreate() {
|
||||
this.editor = { visible: true, id: null, readonly: false };
|
||||
},
|
||||
openEdit(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: false };
|
||||
},
|
||||
openView(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: true };
|
||||
},
|
||||
async handleDelete(row) {
|
||||
await this.$confirm(`确定删除对账单“${row.reconciliationNo}”吗?`, '删除确认', {
|
||||
type: 'warning',
|
||||
});
|
||||
await api.remove(row.id);
|
||||
this.$message.success('删除成功');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleComplete(row) {
|
||||
await this.$confirm('完成后对账单将不可修改,是否继续?', '完成对账', { type: 'warning' });
|
||||
await api.complete(row.id);
|
||||
this.$message.success('对账单确认完成');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleExport() {
|
||||
const { data } = await api.getList(1, 100000, {
|
||||
...this.query,
|
||||
settlementType: this.settlementType,
|
||||
});
|
||||
const exportRows = (data.records || []).map(item => ({
|
||||
对账单号: item.reconciliationNo,
|
||||
付款方: item.payerName,
|
||||
收款方: item.payeeName,
|
||||
项目名称: item.projectName,
|
||||
所属组织: item.deptName,
|
||||
合同编号: item.contractNo,
|
||||
合同名称: item.contractName,
|
||||
结算金额: Number(item.settlementAmount || 0).toFixed(2),
|
||||
对账模式: item.reconciliationModeName,
|
||||
账单总数: item.externalBillCount,
|
||||
匹配数: item.matchedCount,
|
||||
对账状态: item.reconciliationStatusName,
|
||||
创建人: item.createUserName,
|
||||
创建时间: item.createTime,
|
||||
}));
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(exportRows), '运输对账');
|
||||
XLSX.writeFile(workbook, `运输对账${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`);
|
||||
},
|
||||
isEditable(row) {
|
||||
return row.reconciliationStatus === 'unfinished';
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
formatMoney(value, currency = 'RMB') {
|
||||
return `${Number(value || 0).toFixed(2)} ${currency || 'RMB'}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.reconciliation-page__search {
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.reconciliation-page__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px 24px;
|
||||
align-items: start;
|
||||
}
|
||||
.reconciliation-page__search :deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.reconciliation-page__search :deep(.el-form-item__label) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.reconciliation-page__search :deep(.el-input),
|
||||
.reconciliation-page__search :deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
.reconciliation-page__search-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.reconciliation-page__table-panel {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.reconciliation-page__toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
padding: 12px;
|
||||
}
|
||||
.reconciliation-page__toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.reconciliation-page__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.reconciliation-page__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 12px;
|
||||
}
|
||||
.reconciliation-page :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
background: #fff;
|
||||
}
|
||||
.reconciliation-page :deep(.el-table__body tr:nth-child(even) > td.el-table__cell),
|
||||
.reconciliation-page :deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--left),
|
||||
.reconciliation-page :deep(.el-table__body tr:nth-child(even) > td.el-table-fixed-column--right) {
|
||||
background: #fafafa;
|
||||
}
|
||||
:deep(.reconciliation-page.basic-container .basic-container__card > .el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
@media screen and (max-width: 1200px) {
|
||||
.reconciliation-page__search-grid {
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 760px) {
|
||||
.reconciliation-page__search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
<section class="exception-disposal-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
|
||||
<div class="exception-disposal-page__search-grid">
|
||||
<el-form-item v-for="field in searchFields" :key="field.prop" :label="field.label">
|
||||
<el-form-item v-for="field in visibleSearchFields" :key="field.prop" :label="field.label">
|
||||
<el-date-picker
|
||||
v-if="field.type === 'daterange'"
|
||||
v-model="query[field.prop]"
|
||||
@@ -41,6 +41,9 @@
|
||||
<div class="exception-disposal-page__search-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button text :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch">
|
||||
{{ searchExpanded ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -241,7 +244,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Rank, Refresh, Setting } from '@element-plus/icons-vue';
|
||||
import { ArrowDown, ArrowUp, Rank, Refresh, Setting } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import {
|
||||
detailFields,
|
||||
@@ -259,6 +262,8 @@ export default {
|
||||
Rank,
|
||||
Refresh,
|
||||
Setting,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
searchFields,
|
||||
tableColumns,
|
||||
detailFields,
|
||||
@@ -267,6 +272,7 @@ export default {
|
||||
query: {},
|
||||
rows: [],
|
||||
selection: [],
|
||||
searchExpanded: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
dialog: { visible: false, mode: 'follow', loading: false, submitting: false },
|
||||
detail: {},
|
||||
@@ -281,6 +287,9 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
photoUrls() {
|
||||
return this.parsePhotos(this.detail.scenePhotos);
|
||||
},
|
||||
@@ -317,6 +326,9 @@ export default {
|
||||
this.query = {};
|
||||
this.handleSearch();
|
||||
},
|
||||
toggleSearch() {
|
||||
this.searchExpanded = !this.searchExpanded;
|
||||
},
|
||||
handleSizeChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
@@ -426,12 +438,17 @@ export default {
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
min-width: 160px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.exception-disposal-page__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(260px, 1fr));
|
||||
gap: 8px 36px;
|
||||
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||
gap: 8px 24px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
@@ -450,6 +467,7 @@ export default {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.exception-disposal-page__table-panel {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
:readonly="readonly"
|
||||
:headers="uploadHeaders"
|
||||
large
|
||||
@success="url => setImage('qualificationFront', url)"
|
||||
@success="url => handleQualificationUploadSuccess('qualificationFront', url)"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -411,7 +411,7 @@
|
||||
:readonly="readonly"
|
||||
:headers="uploadHeaders"
|
||||
large
|
||||
@success="url => setImage('qualificationBack', url)"
|
||||
@success="url => handleQualificationUploadSuccess('qualificationBack', url)"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -504,6 +504,7 @@ import {
|
||||
getExpiryStat,
|
||||
recognitionIDCard,
|
||||
recognitionTransportCertificates,
|
||||
recognizeBaiduOcr,
|
||||
} from '@/api/transportCapacity/driver';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
@@ -754,6 +755,10 @@ export default {
|
||||
this.qualificationTypeOptions = (res.data.data || [])
|
||||
.map(item => item.dictValue)
|
||||
.filter(Boolean);
|
||||
const matchedType = this.matchQualificationType(this.driverForm.qualificationType);
|
||||
if (matchedType) {
|
||||
this.driverForm.qualificationType = matchedType;
|
||||
}
|
||||
});
|
||||
},
|
||||
formatDeptOptions(tree = [], level = 0) {
|
||||
@@ -953,12 +958,53 @@ export default {
|
||||
},
|
||||
handleIdCardUploadSuccess(prop, url) {
|
||||
this.setImage(prop, url);
|
||||
this.recognizeIdCard(url);
|
||||
if (prop !== 'idCardFront') {
|
||||
return;
|
||||
}
|
||||
this.recognizeBaiduOcr(url, 'id_card', 'front', '身份证', data => {
|
||||
this.applyIdCardRecognition(data);
|
||||
});
|
||||
},
|
||||
handleDrivingLicenseUploadSuccess(prop, url) {
|
||||
this.setImage(prop, url);
|
||||
this.drivingLicenseUploads[prop] = url;
|
||||
this.recognizeDrivingLicense();
|
||||
this.recognizeBaiduOcr(
|
||||
url,
|
||||
'driving_license',
|
||||
prop === 'drivingLicenseBack' ? 'back' : 'front',
|
||||
'驾驶证',
|
||||
data => {
|
||||
this.applyDrivingLicenseRecognition(data);
|
||||
}
|
||||
);
|
||||
},
|
||||
handleQualificationUploadSuccess(prop, url) {
|
||||
this.setImage(prop, url);
|
||||
this.recognizeBaiduOcr(url, 'general', '', '从业资格证', data => {
|
||||
this.applyQualificationRecognition(data);
|
||||
});
|
||||
},
|
||||
recognizeBaiduOcr(url, type, side, documentName, applyRecognition) {
|
||||
if (!url) {
|
||||
this.$message.warning(`${documentName}图片上传成功,未获取到图片地址,无法自动识别`);
|
||||
return;
|
||||
}
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: `${documentName}识别中`,
|
||||
background: 'rgba(255, 255, 255, 0.7)',
|
||||
});
|
||||
recognizeBaiduOcr(url, type, side)
|
||||
.then(res => {
|
||||
applyRecognition(res.data.data?.result || {});
|
||||
this.$message.success(`${documentName}识别完成`);
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.warning(`${documentName}图片上传成功,自动识别失败,请手动填写相关信息`);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.close();
|
||||
});
|
||||
},
|
||||
recognizeIdCard(url = '') {
|
||||
if (!url) {
|
||||
@@ -1040,11 +1086,11 @@ export default {
|
||||
const validDateStart =
|
||||
data.driverLicenseValidDateStart ||
|
||||
data.drivingLicenseStartDate ||
|
||||
this.getOcrValue(data, ['date_vaild_start', '有效期限(起始时间)']);
|
||||
this.getOcrValue(data, ['date_vaild_start', '有效期限(起始时间)', '有效起始日期']);
|
||||
const validDateEnd =
|
||||
data.driverLicenseValidDateEnd ||
|
||||
data.drivingLicenseEndDate ||
|
||||
this.getOcrValue(data, ['date_vaild_end', '有效期限(终止时间)', '有效期限(终⽌时间)']);
|
||||
this.getOcrValue(data, ['date_vaild_end', '有效期限(终止时间)', '有效期限(终⽌时间)', '有效截止日期']);
|
||||
if (driverName && !this.driverForm.driverName) {
|
||||
this.driverForm.driverName = driverName;
|
||||
}
|
||||
@@ -1067,6 +1113,7 @@ export default {
|
||||
this.driverForm.drivingLicenseEndDate = this.normalizeBirthday(validDateEnd);
|
||||
this.driverForm.drivingLicenseLongTerm = 0;
|
||||
}
|
||||
this.applyDrivingLicenseValidity(data);
|
||||
this.$nextTick(() => {
|
||||
[
|
||||
'driverName',
|
||||
@@ -1080,6 +1127,21 @@ export default {
|
||||
});
|
||||
});
|
||||
},
|
||||
applyDrivingLicenseValidity(data = {}) {
|
||||
const validity = this.getOcrValue(data, ['有效期限', '有效期']);
|
||||
if (!validity) return;
|
||||
const dateList = validity.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || [];
|
||||
if (!this.driverForm.drivingLicenseStartDate && dateList[0]) {
|
||||
this.driverForm.drivingLicenseStartDate = this.normalizeBirthday(dateList[0]);
|
||||
}
|
||||
if (/(长期|永久)/.test(validity)) {
|
||||
this.driverForm.drivingLicenseLongTerm = 1;
|
||||
this.driverForm.drivingLicenseEndDate = '';
|
||||
} else if (!this.driverForm.drivingLicenseEndDate && dateList[1]) {
|
||||
this.driverForm.drivingLicenseEndDate = this.normalizeBirthday(dateList[1]);
|
||||
this.driverForm.drivingLicenseLongTerm = 0;
|
||||
}
|
||||
},
|
||||
applyIdCardRecognition(data = {}) {
|
||||
const driverName = data.name || data.driverName || this.getOcrValue(data, ['name', '姓名']);
|
||||
const idCardNo = String(
|
||||
@@ -1093,8 +1155,7 @@ export default {
|
||||
data.nation ||
|
||||
data.ethnicGroup ||
|
||||
this.getOcrValue(data, ['ethnic_group', 'nation', '民族']);
|
||||
const ocrBirthday =
|
||||
data.birthday || data.birthDate || this.getOcrValue(data, ['date', '出生']);
|
||||
const ocrAddress = data.address || this.getOcrValue(data, ['address', '住址', '地址']);
|
||||
if (driverName) {
|
||||
this.driverForm.driverName = driverName;
|
||||
}
|
||||
@@ -1104,8 +1165,8 @@ export default {
|
||||
if (ocrNation) {
|
||||
this.driverForm.nation = this.normalizeNation(ocrNation);
|
||||
}
|
||||
if (ocrBirthday) {
|
||||
this.driverForm.birthday = this.normalizeBirthday(ocrBirthday);
|
||||
if (ocrAddress) {
|
||||
this.driverForm.address = String(ocrAddress).trim();
|
||||
}
|
||||
if (idCardNo) {
|
||||
this.driverForm.idCardNo = idCardNo;
|
||||
@@ -1113,7 +1174,7 @@ export default {
|
||||
this.driverForm.qualificationNo = idCardNo;
|
||||
}
|
||||
const idCardInfo = this.parseIdCardInfo(idCardNo);
|
||||
if (!this.driverForm.birthday && idCardInfo.birthday) {
|
||||
if (idCardInfo.birthday) {
|
||||
this.driverForm.birthday = idCardInfo.birthday;
|
||||
}
|
||||
if (!this.driverForm.gender && idCardInfo.gender) {
|
||||
@@ -1121,11 +1182,17 @@ export default {
|
||||
}
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
['driverName', 'idCardNo', 'birthday', 'gender', 'nation', 'qualificationNo'].forEach(
|
||||
prop => {
|
||||
[
|
||||
'driverName',
|
||||
'idCardNo',
|
||||
'birthday',
|
||||
'gender',
|
||||
'nation',
|
||||
'qualificationNo',
|
||||
'address',
|
||||
].forEach(prop => {
|
||||
this.$refs.driverForm?.validateField(prop);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
getOcrValue(data = {}, keys = []) {
|
||||
@@ -1136,7 +1203,102 @@ export default {
|
||||
const description = String(info.description || '');
|
||||
return keys.some(value => key === String(value).toLowerCase() || description === value);
|
||||
});
|
||||
return item ? item.value || '' : '';
|
||||
if (item) return item.value || '';
|
||||
const wordsResult = data.words_result || data.wordsResult || {};
|
||||
const normalizedKeys = keys.map(key => String(key).toLowerCase());
|
||||
if (Array.isArray(wordsResult)) {
|
||||
const wordItem = wordsResult.find(info =>
|
||||
normalizedKeys.includes(String(info.key || info.name || '').toLowerCase())
|
||||
);
|
||||
return wordItem?.words || wordItem?.value || '';
|
||||
}
|
||||
const wordEntry = Object.entries(wordsResult).find(([key]) =>
|
||||
normalizedKeys.includes(String(key).toLowerCase())
|
||||
);
|
||||
if (!wordEntry) return '';
|
||||
const value = wordEntry[1];
|
||||
return typeof value === 'object' ? value.words || value.value || '' : value || '';
|
||||
},
|
||||
applyQualificationRecognition(data = {}) {
|
||||
const qualificationType = this.getQualificationType(data);
|
||||
const qualificationNo = this.getQualificationNo(data);
|
||||
const matchedType = this.matchQualificationType(qualificationType);
|
||||
if (matchedType) {
|
||||
this.driverForm.qualificationType = matchedType;
|
||||
} else if (qualificationType && !this.qualificationTypeOptions.length) {
|
||||
this.driverForm.qualificationType = qualificationType;
|
||||
}
|
||||
if (qualificationNo && !this.driverForm.qualificationNo) {
|
||||
this.driverForm.qualificationNo = String(qualificationNo).replace(/\s/g, '');
|
||||
}
|
||||
const words = this.getBaiduOcrWords(data).join(' ');
|
||||
const dateList = words.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || [];
|
||||
if (/(长期|永久有效)/.test(words)) {
|
||||
this.driverForm.qualificationLongTerm = 1;
|
||||
this.driverForm.qualificationEndDate = '';
|
||||
} else if (dateList.length) {
|
||||
this.driverForm.qualificationEndDate = this.normalizeBirthday(dateList[dateList.length - 1]);
|
||||
this.driverForm.qualificationLongTerm = 0;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
['qualificationType', 'qualificationNo', 'qualificationEndDate'].forEach(prop => {
|
||||
this.$refs.driverForm?.validateField(prop);
|
||||
});
|
||||
});
|
||||
},
|
||||
getQualificationType(data = {}) {
|
||||
const fieldValue = this.getOcrValue(data, ['从业资格类别', '从业资格类型']);
|
||||
if (fieldValue) return String(fieldValue).trim();
|
||||
const words = this.getBaiduOcrWords(data);
|
||||
const labelIndex = words.findIndex(word => /从业资格类别|从业资格类型/.test(word));
|
||||
if (labelIndex < 0) return '';
|
||||
const firstValue = words[labelIndex].replace(/^.*?(?:从业资格类别|从业资格类型)\s*[::]?/, '').trim();
|
||||
const values = firstValue ? [firstValue] : [];
|
||||
for (let index = labelIndex + 1; index < words.length; index += 1) {
|
||||
const word = String(words[index] || '').trim();
|
||||
if (!word || /^(有效起始日期|有效期限|核发机关|继续教育信息|诚信考核信息)/.test(word)) {
|
||||
break;
|
||||
}
|
||||
values.push(word);
|
||||
}
|
||||
return values.join('').replace(/[,,;;]+$/, '').trim();
|
||||
},
|
||||
matchQualificationType(value = '') {
|
||||
const text = String(value || '').replace(/\s/g, '');
|
||||
if (!text) return '';
|
||||
const options = this.qualificationTypeOptions || [];
|
||||
const normalizedOptions = options.map(option => ({
|
||||
value: option,
|
||||
text: String(option).replace(/\s/g, ''),
|
||||
}));
|
||||
const exact = normalizedOptions.find(option => option.text === text);
|
||||
if (exact) return exact.value;
|
||||
const candidates = text.split(/[,,;;]/).filter(Boolean);
|
||||
const matched = normalizedOptions.find(option =>
|
||||
candidates.some(candidate => candidate.includes(option.text) || option.text.includes(candidate))
|
||||
);
|
||||
return matched?.value || '';
|
||||
},
|
||||
getQualificationNo(data = {}) {
|
||||
const fieldValue = this.getOcrValue(data, ['从业资格证号', '资格证号', '证书编号']);
|
||||
if (fieldValue) return fieldValue;
|
||||
const words = this.getBaiduOcrWords(data);
|
||||
const labelIndex = words.findIndex(word => /^(?:(?:从业)?资格证号?|证书编号)[::]?$/.test(word));
|
||||
if (labelIndex >= 0 && words[labelIndex + 1]) {
|
||||
return words[labelIndex + 1];
|
||||
}
|
||||
const certificateLine = words.find(word => /(?:从业)?资格证号?|证书编号/.test(word));
|
||||
const match = certificateLine?.match(/(?:(?:从业)?资格证号?|证书编号)\s*[::]?\s*([A-Z0-9-]{6,})/i);
|
||||
return match?.[1] || '';
|
||||
},
|
||||
getBaiduOcrWords(data = {}) {
|
||||
const wordsResult = data.words_result || data.wordsResult || {};
|
||||
if (Array.isArray(wordsResult)) {
|
||||
return wordsResult.map(item => item.words || item.value || '').filter(Boolean);
|
||||
}
|
||||
return Object.values(wordsResult)
|
||||
.map(item => (typeof item === 'object' ? item.words || item.value || '' : item || ''))
|
||||
.filter(Boolean);
|
||||
},
|
||||
normalizeNation(value = '') {
|
||||
const nation = String(value || '').trim();
|
||||
|
||||
@@ -500,7 +500,7 @@
|
||||
:headers="uploadHeaders"
|
||||
large
|
||||
class-prefix="ship"
|
||||
@success="url => setImage('ownershipCertImage', url)"
|
||||
@success="url => handleShipCertificateUploadSuccess('ownershipCertImage', url)"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
@@ -512,7 +512,7 @@
|
||||
:headers="uploadHeaders"
|
||||
large
|
||||
class-prefix="ship"
|
||||
@success="url => setImage('safetyCertImage', url)"
|
||||
@success="url => handleShipCertificateUploadSuccess('safetyCertImage', url)"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
@@ -524,7 +524,7 @@
|
||||
:headers="uploadHeaders"
|
||||
large
|
||||
class-prefix="ship"
|
||||
@success="url => setImage('nationalityCertImage', url)"
|
||||
@success="url => handleShipCertificateUploadSuccess('nationalityCertImage', url)"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
@@ -536,7 +536,7 @@
|
||||
:headers="uploadHeaders"
|
||||
large
|
||||
class-prefix="ship"
|
||||
@success="url => setImage('safeManningCertImage', url)"
|
||||
@success="url => handleShipCertificateUploadSuccess('safeManningCertImage', url)"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
@@ -551,7 +551,7 @@
|
||||
:headers="uploadHeaders"
|
||||
large
|
||||
class-prefix="ship"
|
||||
@success="url => setImage('leaseContractImage', url)"
|
||||
@success="url => handleShipCertificateUploadSuccess('leaseContractImage', url)"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
@@ -563,7 +563,7 @@
|
||||
:headers="uploadHeaders"
|
||||
large
|
||||
class-prefix="ship"
|
||||
@success="url => setImage('businessTransportCertImage', url)"
|
||||
@success="url => handleShipCertificateUploadSuccess('businessTransportCertImage', url)"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -582,6 +582,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import NProgress from 'nprogress';
|
||||
import { ElLoading } from 'element-plus';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { option } from '@/option/transportCapacity/transport-ship';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
@@ -592,6 +593,7 @@ import {
|
||||
remove,
|
||||
changeStatus,
|
||||
getExpiryStat,
|
||||
recognizeBaiduOcr,
|
||||
} from '@/api/transportCapacity/transport-ship';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { getToken } from '@/utils/auth';
|
||||
@@ -893,6 +895,155 @@ export default {
|
||||
this.shipForm[prop] = url;
|
||||
this.$refs.shipForm?.validateField(prop);
|
||||
},
|
||||
handleShipCertificateUploadSuccess(prop, url) {
|
||||
this.setImage(prop, url);
|
||||
const documentNameMap = {
|
||||
ownershipCertImage: '船舶所有权证书',
|
||||
safetyCertImage: '内河船舶安全与环保证书',
|
||||
nationalityCertImage: '船舶国籍证书',
|
||||
safeManningCertImage: '内河船舶最低安全配员证书',
|
||||
leaseContractImage: '光船租赁登记证书',
|
||||
businessTransportCertImage: '船舶营业运输证',
|
||||
};
|
||||
this.recognizeBaiduOcr(url, documentNameMap[prop], data => {
|
||||
this.applyShipCertificateRecognition(prop, data);
|
||||
});
|
||||
},
|
||||
recognizeBaiduOcr(url, documentName, applyRecognition) {
|
||||
if (!url) {
|
||||
this.$message.warning(`${documentName}图片上传成功,未获取到图片地址,无法自动识别`);
|
||||
return;
|
||||
}
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: `${documentName}识别中`,
|
||||
background: 'rgba(255, 255, 255, 0.7)',
|
||||
});
|
||||
recognizeBaiduOcr(url, 'general')
|
||||
.then(res => {
|
||||
applyRecognition(res.data.data?.result || {});
|
||||
this.$message.success(`${documentName}识别完成`);
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.warning(`${documentName}图片上传成功,自动识别失败,请手动填写证书信息`);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.close();
|
||||
});
|
||||
},
|
||||
applyShipCertificateRecognition(prop, data = {}) {
|
||||
const words = this.getBaiduOcrWords(data);
|
||||
switch (prop) {
|
||||
case 'ownershipCertImage':
|
||||
this.fillShipCertificateText(words, 'ownershipRegistrationNo', ['登记号码', '登记号']);
|
||||
this.fillShipCertificateText(words, 'initialRegistrationNo', ['初次登记号码', '初始登记号码']);
|
||||
this.fillShipCertificateText(words, 'shipOwner', ['船舶所有人', '所有人']);
|
||||
this.fillShipCertificateText(words, 'shipIdentifierNo', ['船舶识别号', '船舶识别号码']);
|
||||
this.fillShipCertificateDate(words, 'ownershipAcquisitionDate', ['取得所有权日期', '取得日期']);
|
||||
break;
|
||||
case 'safetyCertImage':
|
||||
this.fillShipCertificateNumber(words, 'grossTonnage', ['总吨', '总吨位']);
|
||||
this.fillShipCertificateNumber(words, 'netTonnage', ['净吨', '净吨位']);
|
||||
this.fillShipCertificateText(words, 'shipInspectionNo', ['船检登记号', '船检证书号']);
|
||||
this.fillShipCertificateText(words, 'shipType', ['船舶类型']);
|
||||
break;
|
||||
case 'nationalityCertImage':
|
||||
this.fillShipCertificateDateRange(
|
||||
words,
|
||||
'nationalityCertStartDate',
|
||||
'nationalityCertEndDate',
|
||||
'nationalityCertLongTerm'
|
||||
);
|
||||
break;
|
||||
case 'safeManningCertImage':
|
||||
this.fillShipCertificateDateRange(
|
||||
words,
|
||||
'safeManningCertStartDate',
|
||||
'safeManningCertEndDate',
|
||||
'safeManningCertLongTerm'
|
||||
);
|
||||
break;
|
||||
case 'leaseContractImage':
|
||||
this.fillShipCertificateDate(words, 'leaseStartDate', ['起租日期', '租赁开始日期']);
|
||||
this.fillShipCertificateDate(words, 'leaseEndDate', ['终止日期', '租赁终止日期']);
|
||||
this.fillShipCertificateText(words, 'shipLessee', ['船舶承租人', '承租人']);
|
||||
break;
|
||||
case 'businessTransportCertImage':
|
||||
this.fillShipCertificateText(words, 'businessTransportCertNo', ['证书编号', '运输证号']);
|
||||
this.fillShipCertificateDate(words, 'businessTransportCertIssueDate', ['发证日期']);
|
||||
this.fillShipCertificateDate(words, 'businessTransportCertEndDate', ['有效期至', '有效期限']);
|
||||
this.fillShipCertificateText(words, 'shipOperator', ['船舶经营人', '经营人']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
getBaiduOcrWords(data = {}) {
|
||||
const wordsResult = data.words_result || data.wordsResult || {};
|
||||
if (Array.isArray(wordsResult)) {
|
||||
return wordsResult.map(item => item.words || item.value || '').filter(Boolean);
|
||||
}
|
||||
return Object.entries(wordsResult)
|
||||
.flatMap(([key, value]) => [key, typeof value === 'object' ? value.words || value.value || '' : value])
|
||||
.filter(Boolean);
|
||||
},
|
||||
getShipCertificateOcrValue(words = [], labels = []) {
|
||||
const normalizedWords = words.map(word => String(word || '').trim()).filter(Boolean);
|
||||
for (const label of labels) {
|
||||
const labelIndex = normalizedWords.findIndex(word =>
|
||||
new RegExp(`^${label}\\s*[::]?$`).test(word)
|
||||
);
|
||||
if (labelIndex >= 0 && normalizedWords[labelIndex + 1]) {
|
||||
return normalizedWords[labelIndex + 1];
|
||||
}
|
||||
const valueLine = normalizedWords.find(word =>
|
||||
new RegExp(`${label}\\s*[::]?\\s*(.+)$`).test(word)
|
||||
);
|
||||
if (valueLine) {
|
||||
return valueLine.replace(new RegExp(`^.*${label}\\s*[::]?\\s*`), '').trim();
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
fillShipCertificateText(words, prop, labels) {
|
||||
const value = this.getShipCertificateOcrValue(words, labels);
|
||||
if (value) this.shipForm[prop] = value.replace(/\s/g, '');
|
||||
},
|
||||
fillShipCertificateNumber(words, prop, labels) {
|
||||
const value = this.getShipCertificateOcrValue(words, labels);
|
||||
const number = Number(String(value || '').replace(/[^\d.]/g, ''));
|
||||
if (!Number.isNaN(number) && value) this.shipForm[prop] = number;
|
||||
},
|
||||
fillShipCertificateDate(words, prop, labels) {
|
||||
const value = this.getShipCertificateOcrValue(words, labels);
|
||||
const date = this.getShipCertificateDates(value)[0];
|
||||
if (date) this.shipForm[prop] = date;
|
||||
},
|
||||
fillShipCertificateDateRange(words, startProp, endProp, longTermProp) {
|
||||
const text =
|
||||
this.getShipCertificateOcrValue(words, ['证书有效期', '有效期限', '有效期至', '有效期']) ||
|
||||
words.join(' ');
|
||||
if (/(长期|永久)/.test(text)) {
|
||||
this.shipForm[longTermProp] = 1;
|
||||
this.shipForm[startProp] = '';
|
||||
this.shipForm[endProp] = '';
|
||||
return;
|
||||
}
|
||||
const dateList = this.getShipCertificateDates(text);
|
||||
if (dateList[0] && dateList[1]) {
|
||||
this.shipForm[startProp] = dateList[0];
|
||||
this.shipForm[endProp] = dateList[dateList.length - 1];
|
||||
this.shipForm[longTermProp] = 0;
|
||||
}
|
||||
},
|
||||
getShipCertificateDates(value = '') {
|
||||
return (String(value || '').match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || []).map(
|
||||
date => {
|
||||
const [, year, month, day] = date.match(/(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})/) || [];
|
||||
return year ? `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}` : '';
|
||||
}
|
||||
);
|
||||
},
|
||||
handleSubmit() {
|
||||
this.$refs.shipForm.validate(valid => {
|
||||
if (!valid) return;
|
||||
|
||||
@@ -496,7 +496,7 @@
|
||||
:headers="uploadHeaders"
|
||||
large
|
||||
class-prefix="vehicle"
|
||||
@success="url => setImage('roadTransportCertImage', url)"
|
||||
@success="url => handleVehicleCertificateUploadSuccess('roadTransportCertImage', url)"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
@@ -541,6 +541,7 @@ import {
|
||||
auditCertification,
|
||||
getExpiryStat,
|
||||
recognitionTransportCertificates,
|
||||
recognizeBaiduOcr,
|
||||
} from '@/api/transportCapacity/transport-vehicle';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { exportBlob } from '@/api/common';
|
||||
@@ -875,17 +876,57 @@ export default {
|
||||
handleVehicleCertificateUploadSuccess(prop, url) {
|
||||
this.setImage(prop, url);
|
||||
this.vehicleCertificateUploads[prop] = url;
|
||||
if (prop.startsWith('drivingLicense')) {
|
||||
if (prop.endsWith('Back')) {
|
||||
this.recognizeBaiduVehicleOcr(url, 'general', '', '行驶证', data => {
|
||||
this.applyBaiduVehicleLicenseGeneralRecognition(data);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const side = prop === 'drivingLicenseViceFront' ? 'back' : 'front';
|
||||
this.recognizeBaiduVehicleOcr(url, 'vehicle_license', side, '行驶证', data => {
|
||||
this.applyBaiduVehicleLicenseRecognition(data);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (prop === 'roadTransportCertImage') {
|
||||
this.recognizeBaiduVehicleOcr(
|
||||
url,
|
||||
'road_transport_certificate',
|
||||
'',
|
||||
'道路运输证',
|
||||
data => {
|
||||
this.applyBaiduRoadTransportCertificateRecognition(data);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.recognizeVehicleCertificates();
|
||||
},
|
||||
recognizeBaiduVehicleOcr(url, type, side, documentName, applyRecognition) {
|
||||
if (!url) {
|
||||
this.$message.warning(`${documentName}图片上传成功,未获取到图片地址,无法自动识别`);
|
||||
return;
|
||||
}
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: `${documentName}识别中`,
|
||||
background: 'rgba(255, 255, 255, 0.7)',
|
||||
});
|
||||
recognizeBaiduOcr(url, type, side)
|
||||
.then(res => {
|
||||
applyRecognition(res.data.data?.result || {});
|
||||
this.$message.success(`${documentName}识别完成`);
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.warning(`${documentName}图片上传成功,自动识别失败,请手动填写车辆资质信息`);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.close();
|
||||
});
|
||||
},
|
||||
recognizeVehicleCertificates() {
|
||||
const objectKeys = [
|
||||
this.vehicleCertificateUploads.drivingLicenseImage || this.vehicleForm.drivingLicenseImage,
|
||||
this.vehicleCertificateUploads.drivingLicenseMainBack ||
|
||||
this.vehicleForm.drivingLicenseMainBack,
|
||||
this.vehicleCertificateUploads.drivingLicenseViceFront ||
|
||||
this.vehicleForm.drivingLicenseViceFront,
|
||||
this.vehicleCertificateUploads.drivingLicenseViceBack ||
|
||||
this.vehicleForm.drivingLicenseViceBack,
|
||||
this.vehicleCertificateUploads.registrationImage || this.vehicleForm.registrationImage,
|
||||
].filter(Boolean);
|
||||
if (!objectKeys.length) {
|
||||
@@ -909,6 +950,168 @@ export default {
|
||||
loading.close();
|
||||
});
|
||||
},
|
||||
applyBaiduVehicleLicenseRecognition(data = {}) {
|
||||
const plateNo = this.getBaiduOcrValue(data, ['号牌号码', '车牌号码']);
|
||||
const vehicleType = this.getBaiduOcrValue(data, ['车辆类型']);
|
||||
const drivingLicenseNo = this.getBaiduOcrValue(data, ['档案编号']);
|
||||
const inspectionValidity = this.getBaiduOcrValue(data, ['检验有效期至', '检验有效期', '检验记录']);
|
||||
const registrationDate = this.getBaiduOcrValue(data, ['注册日期']);
|
||||
const registrationNo = this.getBaiduOcrValue(data, ['车辆识别代号', '车架号']);
|
||||
const approvedLoadKg = this.getBaiduOcrValue(data, ['核定载质量']);
|
||||
const outerDimensions = this.getBaiduOcrValue(data, ['外廓尺寸']);
|
||||
if (plateNo) {
|
||||
this.vehicleForm.plateNo = String(plateNo).replace(/\s/g, '').toUpperCase();
|
||||
this.splitPlateNo();
|
||||
this.syncPlateNo(false);
|
||||
}
|
||||
if (vehicleType) {
|
||||
this.vehicleForm.vehicleType = this.normalizeVehicleType(vehicleType);
|
||||
}
|
||||
if (drivingLicenseNo) {
|
||||
this.vehicleForm.drivingLicenseNo = String(drivingLicenseNo).replace(/\s/g, '');
|
||||
}
|
||||
this.applyDrivingLicenseValidity(inspectionValidity);
|
||||
if (registrationDate) {
|
||||
this.vehicleForm.registrationDate = this.normalizeDate(registrationDate);
|
||||
}
|
||||
if (registrationNo) {
|
||||
this.vehicleForm.registrationNo = String(registrationNo).replace(/\s/g, '');
|
||||
}
|
||||
if (approvedLoadKg) {
|
||||
this.vehicleForm.approvedLoadKg = this.normalizeVehicleNumber(approvedLoadKg);
|
||||
}
|
||||
this.applyOuterDimensions(outerDimensions);
|
||||
this.$nextTick(() => {
|
||||
[
|
||||
'plateNo',
|
||||
'vehicleType',
|
||||
'drivingLicenseNo',
|
||||
'drivingLicenseEndDate',
|
||||
'registrationNo',
|
||||
'approvedLoadKg',
|
||||
'outerLength',
|
||||
'outerWidth',
|
||||
'outerHeight',
|
||||
].forEach(prop => {
|
||||
this.$refs.vehicleForm?.validateField(prop);
|
||||
});
|
||||
});
|
||||
},
|
||||
applyBaiduVehicleLicenseGeneralRecognition(data = {}) {
|
||||
const words = this.getBaiduOcrWords(data);
|
||||
const getValue = labels => this.getBaiduGeneralOcrValue(words, labels);
|
||||
this.applyBaiduVehicleLicenseRecognition({
|
||||
words_result: {
|
||||
号牌号码: { words: getValue(['号牌号码', '车牌号码']) },
|
||||
车辆类型: { words: getValue(['车辆类型']) },
|
||||
档案编号: { words: getValue(['档案编号']) },
|
||||
检验有效期至: { words: getValue(['检验有效期至', '检验有效期', '检验记录']) },
|
||||
注册日期: { words: getValue(['注册日期']) },
|
||||
车辆识别代号: { words: getValue(['车辆识别代号', '车架号']) },
|
||||
核定载质量: { words: getValue(['核定载质量']) },
|
||||
外廓尺寸: { words: getValue(['外廓尺寸']) },
|
||||
},
|
||||
});
|
||||
},
|
||||
normalizeVehicleNumber(value = '') {
|
||||
const number = String(value || '').replace(/,/g, '').match(/-?\d+(?:\.\d+)?/);
|
||||
return number ? number[0] : '';
|
||||
},
|
||||
applyOuterDimensions(value = '') {
|
||||
const dimensions = String(value || '').match(/\d+(?:\.\d+)?/g) || [];
|
||||
if (dimensions[0]) this.vehicleForm.outerLength = dimensions[0];
|
||||
if (dimensions[1]) this.vehicleForm.outerWidth = dimensions[1];
|
||||
if (dimensions[2]) this.vehicleForm.outerHeight = dimensions[2];
|
||||
},
|
||||
applyBaiduRoadTransportCertificateRecognition(data = {}) {
|
||||
const certificateNo = this.getBaiduOcrValue(data, ['道路运输证号', '证号']);
|
||||
const validity = this.getBaiduOcrValue(data, ['有效期至', '有效期限', '有效期']);
|
||||
if (certificateNo) {
|
||||
this.vehicleForm.roadTransportCertNo = String(certificateNo).replace(/\s/g, '');
|
||||
}
|
||||
this.applyRoadTransportCertificateValidity(validity);
|
||||
this.$nextTick(() => {
|
||||
['roadTransportCertNo', 'roadTransportCertEndDate'].forEach(prop => {
|
||||
this.$refs.vehicleForm?.validateField(prop);
|
||||
});
|
||||
});
|
||||
},
|
||||
applyRoadTransportCertificateValidity(validity = '') {
|
||||
const value = String(validity || '');
|
||||
if (!value) return;
|
||||
if (/(长期|永久)/.test(value)) {
|
||||
this.vehicleForm.roadTransportCertLongTerm = 1;
|
||||
this.vehicleForm.roadTransportCertEndDate = '';
|
||||
return;
|
||||
}
|
||||
const dateList = value.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || [];
|
||||
if (dateList.length) {
|
||||
this.vehicleForm.roadTransportCertEndDate = this.normalizeDate(dateList[dateList.length - 1]);
|
||||
this.vehicleForm.roadTransportCertLongTerm = 0;
|
||||
}
|
||||
},
|
||||
applyDrivingLicenseValidity(validity = '') {
|
||||
const value = String(validity || '');
|
||||
if (!value) return;
|
||||
if (/(长期|永久)/.test(value)) {
|
||||
this.vehicleForm.drivingLicenseLongTerm = 1;
|
||||
this.vehicleForm.drivingLicenseEndDate = '';
|
||||
return;
|
||||
}
|
||||
const dateList = value.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || [];
|
||||
if (dateList.length) {
|
||||
this.vehicleForm.drivingLicenseEndDate = this.normalizeDate(dateList[dateList.length - 1]);
|
||||
this.vehicleForm.drivingLicenseLongTerm = 0;
|
||||
}
|
||||
},
|
||||
getBaiduOcrValue(data = {}, keys = []) {
|
||||
const wordsResult = data.words_result || data.wordsResult || {};
|
||||
const normalizedKeys = keys.map(key => String(key).toLowerCase());
|
||||
if (Array.isArray(wordsResult)) {
|
||||
const wordItem = wordsResult.find(item =>
|
||||
normalizedKeys.includes(String(item.key || item.name || '').toLowerCase())
|
||||
);
|
||||
return wordItem?.words || wordItem?.word || wordItem?.value || '';
|
||||
}
|
||||
const wordEntry = Object.entries(wordsResult).find(([key]) =>
|
||||
normalizedKeys.includes(String(key).toLowerCase())
|
||||
);
|
||||
if (!wordEntry) return '';
|
||||
const value = wordEntry[1];
|
||||
if (Array.isArray(value)) {
|
||||
const item = value.find(entry => entry?.words || entry?.word || entry?.value);
|
||||
return item?.words || item?.word || item?.value || '';
|
||||
}
|
||||
return typeof value === 'object' ? value.words || value.word || value.value || '' : value || '';
|
||||
},
|
||||
getBaiduOcrWords(data = {}) {
|
||||
const wordsResult = data.words_result || data.wordsResult || {};
|
||||
if (Array.isArray(wordsResult)) {
|
||||
return wordsResult.map(item => item.words || item.word || item.value || '').filter(Boolean);
|
||||
}
|
||||
return Object.values(wordsResult)
|
||||
.map(item => {
|
||||
if (Array.isArray(item)) {
|
||||
const entry = item.find(value => value?.words || value?.word || value?.value);
|
||||
return entry?.words || entry?.word || entry?.value || '';
|
||||
}
|
||||
return typeof item === 'object' ? item.words || item.word || item.value || '' : item || '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
},
|
||||
getBaiduGeneralOcrValue(words = [], labels = []) {
|
||||
const normalizedWords = words.map(word => String(word || '').trim()).filter(Boolean);
|
||||
for (const label of labels) {
|
||||
const labelIndex = normalizedWords.findIndex(word => word.includes(label));
|
||||
if (labelIndex < 0) continue;
|
||||
const inlineValue = normalizedWords[labelIndex]
|
||||
.replace(new RegExp(`^.*${label}\\s*[::]?`), '')
|
||||
.trim();
|
||||
if (inlineValue) return inlineValue;
|
||||
if (normalizedWords[labelIndex + 1]) return normalizedWords[labelIndex + 1];
|
||||
}
|
||||
return '';
|
||||
},
|
||||
applyVehicleCertificateRecognition(data = {}) {
|
||||
const plateNo = data.drivingPlateNo || data.vehicleLicensePlateNo || '';
|
||||
const vehicleType = data.vehicleLicenseVehicleType || data.vehicleType || '';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -101,19 +101,20 @@
|
||||
</div>
|
||||
<div class="insurance-record-page__upload-text">
|
||||
<strong>上传图片,自动识别填写</strong>
|
||||
<span>支持JPG、PNG、PDF上传</span>
|
||||
<span>支持JPG、PNG、BMP上传</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-upload
|
||||
v-if="hasPermission('insurance_record_ocr')"
|
||||
class="insurance-record-page__upload-action"
|
||||
:action="recognizeUrl"
|
||||
:data="recognizeData"
|
||||
action="/api/blade-resource/oss/endpoint/put-file"
|
||||
name="file"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleRecognizeSuccess"
|
||||
:on-error="handleRecognizeError"
|
||||
accept=".jpg,.jpeg,.png,.pdf"
|
||||
:on-success="handlePolicyUploadSuccess"
|
||||
:on-error="handlePolicyUploadError"
|
||||
:before-upload="validateOcrUpload"
|
||||
accept=".jpg,.jpeg,.png,.bmp"
|
||||
>
|
||||
<el-button class="insurance-record-page__upload-button" type="primary" plain>
|
||||
上传文件
|
||||
@@ -131,19 +132,20 @@
|
||||
</div>
|
||||
<div class="insurance-record-page__upload-text">
|
||||
<strong>上传图片,自动识别填写</strong>
|
||||
<span>支持JPG、PNG、PDF上传</span>
|
||||
<span>支持JPG、PNG、BMP上传</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-upload
|
||||
v-if="hasPermission('insurance_record_ocr')"
|
||||
class="insurance-record-page__upload-action"
|
||||
:action="recognizeUrl"
|
||||
:data="recognizeData"
|
||||
action="/api/blade-resource/oss/endpoint/put-file"
|
||||
name="file"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleRecognizeSuccess"
|
||||
:on-error="handleRecognizeError"
|
||||
accept=".jpg,.jpeg,.png,.pdf"
|
||||
:on-success="handlePolicyUploadSuccess"
|
||||
:on-error="handlePolicyUploadError"
|
||||
:before-upload="validateOcrUpload"
|
||||
accept=".jpg,.jpeg,.png,.bmp"
|
||||
>
|
||||
<el-button class="insurance-record-page__upload-button" type="primary" plain>
|
||||
上传文件
|
||||
@@ -171,9 +173,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { add, getDetail, getList, remove, update } from '@/api/vehicle/insurance-record';
|
||||
import {
|
||||
add,
|
||||
getDetail,
|
||||
getList,
|
||||
recognizeGeneralOcr,
|
||||
remove,
|
||||
update,
|
||||
} from '@/api/vehicle/insurance-record';
|
||||
import { getList as getVehicleList } from '@/api/transportCapacity/transport-vehicle';
|
||||
import { getList as getShipList } from '@/api/transportCapacity/transport-ship';
|
||||
import { getList as getOcrTemplateList } from '@/api/base/insurance-ocr-template';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
@@ -182,6 +192,7 @@ import { getToken } from '@/utils/auth';
|
||||
import { getUploadHeaders } from '@/utils/upload';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { ElLoading } from 'element-plus';
|
||||
import { excelOption, option } from '@/option/vehicle/insurance-record';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
@@ -210,10 +221,12 @@ export default {
|
||||
},
|
||||
selectionList: [],
|
||||
data: [],
|
||||
ocrTemplateOptions: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.initDeptTree();
|
||||
this.initOcrTemplateOptions();
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
@@ -239,15 +252,6 @@ export default {
|
||||
});
|
||||
return ids.join(',');
|
||||
},
|
||||
recognizeUrl() {
|
||||
return '/blade-transport/insurance-record/recognize';
|
||||
},
|
||||
recognizeData() {
|
||||
return {
|
||||
vehicleType: this.form.vehicleType || '车辆',
|
||||
ocrTemplate: this.form.ocrTemplate,
|
||||
};
|
||||
},
|
||||
uploadHeaders() {
|
||||
return getUploadHeaders();
|
||||
},
|
||||
@@ -278,6 +282,22 @@ export default {
|
||||
column.dicData = res.data.data;
|
||||
});
|
||||
},
|
||||
initOcrTemplateOptions() {
|
||||
getOcrTemplateList(1, 100, {})
|
||||
.then(res => {
|
||||
const records = res.data.data?.records || [];
|
||||
this.ocrTemplateOptions = records.map(item => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
mappingConfig: item.mappingConfig || '[]',
|
||||
}));
|
||||
const templateColumn = this.findColumn(this.option.column, 'ocrTemplate');
|
||||
if (templateColumn) templateColumn.dicData = this.ocrTemplateOptions;
|
||||
})
|
||||
.catch(() => {
|
||||
this.ocrTemplateOptions = [];
|
||||
});
|
||||
},
|
||||
fetchVehicleOptions(queryString, callback) {
|
||||
const vehicleType = this.form.vehicleType || '车辆';
|
||||
const request = vehicleType === '船舶' ? getShipList : getVehicleList;
|
||||
@@ -402,7 +422,7 @@ export default {
|
||||
this.boxType = type;
|
||||
if (type === 'add') {
|
||||
this.form.vehicleType = '车辆';
|
||||
this.form.ocrTemplate = '紫金-机动车交强险';
|
||||
this.form.ocrTemplate = this.ocrTemplateOptions[0]?.value || '';
|
||||
}
|
||||
if (['edit', 'view'].includes(type)) {
|
||||
this.isDetailLoading = true;
|
||||
@@ -499,20 +519,143 @@ export default {
|
||||
downloadXls(res.data, '保险记录模板.xlsx');
|
||||
});
|
||||
},
|
||||
handleRecognizeSuccess(res) {
|
||||
if (res && res.success && res.data) {
|
||||
this.form = {
|
||||
...this.form,
|
||||
...res.data,
|
||||
vehicleType: res.data.vehicleType || this.form.vehicleType || '车辆',
|
||||
};
|
||||
this.$message.success('识别完成');
|
||||
handlePolicyUploadSuccess(res) {
|
||||
if (res?.code !== 200 || !res.data) {
|
||||
this.$message.error(res?.msg || '上传失败');
|
||||
return;
|
||||
}
|
||||
const imageUrl = res.data.link || res.data.url || res.data.domain || '';
|
||||
if (!imageUrl) {
|
||||
this.$message.warning('文件上传成功,未获取到文件地址,无法自动识别');
|
||||
return;
|
||||
}
|
||||
this.form.policyFile = imageUrl;
|
||||
this.recognizePolicyFile(imageUrl);
|
||||
},
|
||||
handlePolicyUploadError() {
|
||||
this.$message.error('上传失败');
|
||||
},
|
||||
recognizePolicyFile(imageUrl) {
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '保单识别中',
|
||||
background: 'rgba(255, 255, 255, 0.7)',
|
||||
});
|
||||
recognizeGeneralOcr(imageUrl)
|
||||
.then(res => {
|
||||
const result = res.data.data?.result || {};
|
||||
const filledCount = this.applyTemplateRecognition(result);
|
||||
if (filledCount > 0) {
|
||||
this.$message.success(`保单识别完成,已填充${filledCount}项`);
|
||||
} else {
|
||||
this.$message.warning((res && res.msg) || '识别失败,请手动填写');
|
||||
this.$message.warning('保单识别完成,未匹配到模板字段,请手动填写保险信息');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.warning('文件上传成功,自动识别失败,请手动填写保险信息');
|
||||
})
|
||||
.finally(() => {
|
||||
loading.close();
|
||||
});
|
||||
},
|
||||
applyTemplateRecognition(data = {}) {
|
||||
const template = this.ocrTemplateOptions.find(item => item.value === this.form.ocrTemplate);
|
||||
const mappings = this.parseTemplateMappings(template?.mappingConfig);
|
||||
const words = this.getGeneralOcrWords(data);
|
||||
const fieldPropMap = {
|
||||
保险类型: 'insuranceType',
|
||||
保单号: 'policyNo',
|
||||
开始日期: 'startDate',
|
||||
结束日期: 'endDate',
|
||||
保额: 'insuredAmount',
|
||||
保费: 'premium',
|
||||
发票号: 'invoiceNo',
|
||||
开票日期: 'invoiceDate',
|
||||
备注: 'remark',
|
||||
};
|
||||
let filledCount = 0;
|
||||
mappings.forEach(item => {
|
||||
const prop = fieldPropMap[item.key];
|
||||
const value = this.getMappedOcrValue(words, item.value);
|
||||
if (!prop || !value) return;
|
||||
this.form[prop] = this.normalizeRecognizedValue(prop, value);
|
||||
filledCount += 1;
|
||||
});
|
||||
return filledCount;
|
||||
},
|
||||
parseTemplateMappings(mappingConfig) {
|
||||
try {
|
||||
const mappings = JSON.parse(mappingConfig || '[]');
|
||||
return Array.isArray(mappings)
|
||||
? mappings.filter(
|
||||
item => String(item?.key || '').trim() && String(item?.value || '').trim()
|
||||
)
|
||||
: [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
handleRecognizeError() {
|
||||
this.$message.warning('识别失败,请手动填写');
|
||||
getGeneralOcrWords(data = {}) {
|
||||
const wordsResult = data.words_result || data.wordsResult || [];
|
||||
if (Array.isArray(wordsResult)) {
|
||||
return wordsResult.map(item => item.words || item.value || '').filter(Boolean);
|
||||
}
|
||||
return Object.entries(wordsResult)
|
||||
.flatMap(([key, value]) => [
|
||||
key,
|
||||
typeof value === 'object' ? value.words || value.value || '' : value,
|
||||
])
|
||||
.filter(Boolean);
|
||||
},
|
||||
getMappedOcrValue(words = [], mappingLabel = '') {
|
||||
const label = String(mappingLabel || '').trim();
|
||||
if (!label) return '';
|
||||
const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const normalizedWords = words.map(word => String(word || '').trim()).filter(Boolean);
|
||||
const exactIndex = normalizedWords.findIndex(word =>
|
||||
new RegExp(`^${escapedLabel}\\s*[::]?$`).test(word)
|
||||
);
|
||||
if (exactIndex >= 0) return normalizedWords[exactIndex + 1] || '';
|
||||
const valueLine = normalizedWords.find(word =>
|
||||
new RegExp(`${escapedLabel}\\s*[::]?\\s*(.+)$`).test(word)
|
||||
);
|
||||
if (!valueLine) return '';
|
||||
return valueLine.replace(new RegExp(`^.*?${escapedLabel}\\s*[::]?\\s*`), '').trim();
|
||||
},
|
||||
normalizeRecognizedValue(prop, value) {
|
||||
const text = String(value || '').trim();
|
||||
if (['startDate', 'endDate', 'invoiceDate'].includes(prop)) {
|
||||
const match = text.match(/(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?/);
|
||||
return match
|
||||
? `${match[1]}-${match[2].padStart(2, '0')}-${match[3].padStart(2, '0')}`
|
||||
: text;
|
||||
}
|
||||
if (['insuredAmount', 'premium'].includes(prop)) {
|
||||
return text.replace(/,/g, '').replace(/[^\d.]/g, '');
|
||||
}
|
||||
if (prop === 'insuranceType') {
|
||||
const typeRules = [
|
||||
{ value: '交强险', pattern: /(交强险|交通事故责任强制保险)/ },
|
||||
{ value: '商业险', pattern: /(商业险|商业保险)/ },
|
||||
{ value: '承运人责任险', pattern: /承运人.*责任/ },
|
||||
{ value: '货运险', pattern: /(货运险|货物运输保险)/ },
|
||||
{ value: '船舶险', pattern: /(船舶险|船舶保险)/ },
|
||||
];
|
||||
return typeRules.find(item => item.pattern.test(text))?.value || text;
|
||||
}
|
||||
if (prop === 'remark') return text.slice(0, 200);
|
||||
return text;
|
||||
},
|
||||
validateOcrUpload(file) {
|
||||
if (!this.form.ocrTemplate) {
|
||||
this.$message.warning('请先选择OCR识别模板');
|
||||
return false;
|
||||
}
|
||||
const validType = ['image/jpeg', 'image/png', 'image/jpg', 'image/bmp'].includes(file.type);
|
||||
const validSize = file.size / 1024 / 1024 < 5;
|
||||
if (!validType) this.$message.error('仅支持 JPG、PNG、BMP 图片');
|
||||
if (!validSize) this.$message.error('图片大小不能超过 5MB');
|
||||
return validType && validSize;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user