1、调整配载、总单

2、新增收付款模块
This commit is contained in:
2026-08-22 21:11:48 +08:00
parent 1962bee882
commit 5fcb82c0c0
51 changed files with 11663 additions and 1506 deletions
+6
View File
@@ -24,6 +24,12 @@ export const getDetail = id =>
},
});
export const getCarrierContracts = () =>
request({
url: `${baseUrl}/carrier-contracts`,
method: 'get',
});
export const saveDraft = row =>
request({
url: `${baseUrl}/save-draft`,
+2
View File
@@ -5,6 +5,8 @@ const baseUrl = '/blade-transport/master-order';
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 getCarriers = id =>
request({ url: `${baseUrl}/carriers`, method: 'get', params: { id } });
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
export const saveDraft = data => request({ url: `${baseUrl}/draft`, method: 'post', data });
export const copy = id => request({ url: `${baseUrl}/copy`, method: 'post', params: { id } });
+27
View File
@@ -0,0 +1,27 @@
import request from '@/axios';
const baseUrl = '/blade-transport/bill-ledger';
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 getExpiryCounts = () => request({ url: `${baseUrl}/expiry-counts`, method: 'get' });
export const getAvailableOptions = (keyword, deptId, selectedId) =>
request({
url: `${baseUrl}/available-options`,
method: 'get',
params: { keyword, deptId, selectedId },
});
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
export const billTypeOptions = [
{ label: '开票', value: 'issued' },
{ label: '收票', value: 'received' },
];
export const maturityStatusOptions = [
{ label: '未到期', value: 'unexpired' },
{ label: '今日到期', value: 'due_today' },
{ label: '已到期', value: 'expired' },
];
+21
View File
@@ -0,0 +1,21 @@
import request from '@/axios';
const baseUrl = '/blade-transport/bill-payment';
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 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 approvalStatusOptions = [
{ label: '草稿', value: 'draft' },
{ label: '审批中', value: 'reviewing' },
{ label: '已驳回', value: 'returned' },
{ label: '审批通过', value: 'approved' },
{ label: '已作废', value: 'voided' },
];
+38
View File
@@ -0,0 +1,38 @@
import request from '@/axios';
const baseUrl = '/blade-transport/invoice-application';
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 getSettlementCandidates = keyword =>
request({ url: `${baseUrl}/settlement-candidates`, method: 'get', params: { keyword } });
export const getSettlementDetails = settlementIds =>
request({ url: `${baseUrl}/settlement-details`, method: 'get', params: { settlementIds } });
export const getReceiverInformation = settlementIds =>
request({ url: `${baseUrl}/receiver-information`, method: 'get', params: { settlementIds } });
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 invoiceTypeOptions = [
{ label: '电子专票', value: 'electronic_special' },
{ label: '电子普票', value: 'electronic_normal' },
];
export const approvalStatusOptions = [
{ label: '草稿', value: 'draft' },
{ label: '审批中', value: 'reviewing' },
{ label: '审批通过', value: 'approved' },
{ label: '已驳回', value: 'returned' },
{ label: '已作废', value: 'voided' },
];
export const kingdeeStatusOptions = [
{ label: '未同步', value: 'unsynced' },
{ label: '已同步', value: 'synced' },
{ label: '同步失败', value: 'failed' },
];
+43
View File
@@ -0,0 +1,43 @@
import request from '@/axios';
const baseUrl = '/blade-transport/invoice-receipt';
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 getInvoicePool = keyword =>
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params: { keyword } });
export const getSettlementCandidates = (keyword, receiptId) =>
request({
url: `${baseUrl}/settlement-candidates`,
method: 'get',
params: { keyword, receiptId },
});
export const getReferenceInformation = settlementIds =>
request({
url: `${baseUrl}/reference-information`,
method: 'get',
params: { settlementIds },
});
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 approvalStatusOptions = [
{ label: '草稿', value: 'draft' },
{ label: '审批中', value: 'reviewing' },
{ label: '审批通过', value: 'approved' },
{ label: '已驳回', value: 'returned' },
{ label: '已作废', value: 'voided' },
];
export const kingdeeStatusOptions = [
{ label: '未同步', value: 'unsynced' },
{ label: '已同步', value: 'synced' },
{ label: '同步失败', value: 'failed' },
];
+37
View File
@@ -0,0 +1,37 @@
import request from '@/axios';
const baseUrl = '/blade-transport/payment-application';
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 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 paymentTypeOptions = [
{ label: '项目预付', value: 'project_advance' },
{ label: '进度预付', value: 'progress_advance' },
{ label: '结算付款', value: 'settlement_payment' },
];
export const paymentMethodOptions = [
{ label: '银行转账', value: 'bank_transfer' },
{ label: '银行承兑汇票', value: 'bank_draft' },
{ label: '商业承兑汇票', value: 'commercial_draft' },
];
export const approvalStatusOptions = [
{ label: '草稿', value: 'draft' },
{ label: '审批中', value: 'reviewing' },
{ label: '审批通过', value: 'approved' },
{ label: '已驳回', value: 'returned' },
{ label: '已作废', value: 'voided' },
];
export const kingdeeStatusOptions = [
{ label: '未生成', value: 'unsynced' },
{ label: '已生成', value: 'synced' },
{ label: '生成失败', value: 'failed' },
];
+21
View File
@@ -0,0 +1,21 @@
import request from '@/axios';
const baseUrl = '/blade-transport/receipt-claim-record';
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 updateAttachments = data =>
request({ url: `${baseUrl}/attachments`, method: 'post', data });
export const voidClaim = id => request({ url: `${baseUrl}/void`, method: 'post', params: { id } });
export const claimStatusOptions = [
{ label: '已认领', value: 'claimed' },
{ label: '已作废', value: 'voided' },
];
export const kingdeeBillStatusOptions = [
{ label: '未生成', value: 'none' },
{ label: '审核通过', value: 'approved' },
{ label: '处理失败', value: 'failed' },
];
+21
View File
@@ -0,0 +1,21 @@
import request from '@/axios';
const baseUrl = '/blade-transport/receipt-flow';
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 getSettlementCandidates = (keyword, flowId) =>
request({
url: `${baseUrl}/settlement-candidates`,
method: 'get',
params: { keyword, flowId },
});
export const claim = data => request({ url: `${baseUrl}/claim`, method: 'post', data });
export const sync = () => request({ url: `${baseUrl}/sync`, method: 'post' });
export const claimStatusOptions = [
{ label: '未认领', value: 'unclaimed' },
{ label: '部分认领', value: 'partial' },
{ label: '认领完成', value: 'claimed' },
];
@@ -15,21 +15,16 @@ export const getChangeRecords = (current, size, params) =>
params: { current, size, ...params },
});
export const updateFee = data =>
request({ url: `${baseUrl}/update-fee`, method: 'post', data });
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 adjustFee = data => request({ url: `${baseUrl}/adjust-fee`, method: 'post', data });
export const calculateAdjustedFee = data =>
request({ url: `${baseUrl}/calculate-adjusted-fee`, method: 'post', data });
export const transferSettlement = data =>
request({ url: `${baseUrl}/transfer-settlement`, method: 'post', data });
export const getTransferCandidates = (current, size, params) =>
request({
url: `${baseUrl}/transfer-candidates`,
-17
View File
@@ -40,30 +40,13 @@ export const config = {
enableAllDept: true,
allDeptReadonly: false,
batchDelete: false,
enableContractForm: true,
enableProjectSelect: true,
projectQueryParams: {
approvalStatuses: 'approved,change_approved',
},
enableContractPeriod: true,
enableContractFileUpload: true,
enableCurrentUserHandler: true,
enableBillingPlan: true,
enableSettlementRule: true,
enableReconciliation: false,
enableFeeGenerationMode: true,
enableSettlementConfigTabs: true,
enablePaymentRatio: true,
enableAttachmentTable: true,
enableChangeRecord: true,
enableOrganizationSelect: true,
enableContractCreateActions: true,
enableContractExpiryTags: true,
actions: ['copy'],
statusProp: 'approvalStatus',
statusTextProp: 'approvalStatusName',
customMenuActions: true,
contractNameDetail: true,
detailSections: [
{
title: '基本信息',
+11
View File
@@ -0,0 +1,11 @@
export const billLedgerTableColumns = [
{ prop: 'billNo', label: '票据编号', minWidth: 170 },
{ prop: 'receiverName', label: '收票单位', minWidth: 170 },
{ prop: 'issuerName', label: '出票单位', minWidth: 170 },
{ prop: 'billTypeName', label: '汇票类型', minWidth: 110 },
{ prop: 'faceAmount', label: '票面金额', minWidth: 130, money: true },
{ prop: 'availableBalance', label: '可用余额', minWidth: 130, money: true },
{ prop: 'issueDate', label: '出票日期', minWidth: 120 },
{ prop: 'maturityDate', label: '到期日期', minWidth: 120 },
{ prop: 'remark', label: '备注', minWidth: 180 },
];
+12
View File
@@ -0,0 +1,12 @@
export const billPaymentTableColumns = [
{ prop: 'paymentNo', label: '单据号', minWidth: 160 },
{ prop: 'deptName', label: '使用部门', minWidth: 150 },
{ prop: 'paymentDate', label: '付款日期', minWidth: 120 },
{ prop: 'usedAmount', label: '使用金额', minWidth: 130, money: true },
{ prop: 'remark', label: '备注', minWidth: 180 },
{ prop: 'approvalStatusName', label: '单据状态', minWidth: 110, status: true },
{ prop: 'currentNode', label: '当前节点', minWidth: 120 },
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
{ prop: 'createUserName', label: '创建人', minWidth: 110 },
{ prop: 'createTime', label: '创建时间', minWidth: 170 },
];
+34
View File
@@ -0,0 +1,34 @@
export const invoiceApplicationTableColumns = [
{ prop: 'applicationNo', label: '单据号', minWidth: 170, link: true },
{ prop: 'settlementNos', label: '结算单号', minWidth: 190, link: true },
{ prop: 'projectName', label: '所属项目', minWidth: 140 },
{ prop: 'deptName', label: '所属组织', minWidth: 140 },
{ prop: 'issuerName', label: '开票方', minWidth: 150 },
{ prop: 'receiverName', label: '受票方', minWidth: 150 },
{ prop: 'invoiceAmount', label: '开票金额', minWidth: 130, money: true },
{ prop: 'createUserName', label: '创建人', minWidth: 110 },
{ prop: 'createTime', label: '创建时间', minWidth: 170 },
{ prop: 'approvalStatusName', label: '状态', minWidth: 110, status: true },
{ prop: 'currentNode', label: '当前节点', minWidth: 130 },
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
];
export const invoiceApplicationDetailColumns = [
{ prop: 'documentNo', label: '单据号', minWidth: 180, link: true },
{ prop: 'waybillNo', label: '运单号', minWidth: 160, link: true },
{ prop: 'vehicleNo', label: '车号', minWidth: 120 },
{ prop: 'departureAddress', label: '发货地址', minWidth: 180 },
{ prop: 'arrivalAddress', label: '到货地址', minWidth: 180 },
{ prop: 'actualDepartureTime', label: '实际发货时间', minWidth: 170 },
{ prop: 'actualCompletionTime', label: '实际完成时间', minWidth: 170 },
{ prop: 'transportType', label: '运输类型', minWidth: 120 },
{ prop: 'cargoName', label: '货物名称', minWidth: 140 },
{ prop: 'cargoType', label: '货物类型', minWidth: 130 },
{ prop: 'transportQuantity', label: '运输总量', minWidth: 120 },
{ prop: 'mileage', label: '里程(KM', minWidth: 120 },
{ prop: 'batchNo', label: '批次号', minWidth: 120 },
{ prop: 'freightAmount', label: '运费', minWidth: 120, money: true },
{ prop: 'feeItemsJson', label: '费用项目', minWidth: 180 },
];
+16
View File
@@ -0,0 +1,16 @@
export const invoiceReceiptTableColumns = [
{ prop: 'invoiceNo', label: '发票号', minWidth: 180, link: true },
{ prop: 'invoiceDate', label: '开票日期', minWidth: 120 },
{ prop: 'projectName', label: '所属项目', minWidth: 140 },
{ prop: 'deptName', label: '所属组织', minWidth: 150 },
{ prop: 'payerName', label: '付款方', minWidth: 150 },
{ prop: 'payeeName', label: '收款方', minWidth: 150 },
{ prop: 'invoiceAmount', label: '收票金额', minWidth: 130, money: true },
{ prop: 'approvalStatusName', label: '审核状态', minWidth: 110, status: true },
{ prop: 'currentNode', label: '当前节点', minWidth: 130 },
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
{ prop: 'createUserName', label: '创建人', minWidth: 110 },
{ prop: 'createTime', label: '创建时间', minWidth: 170 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
];
+20
View File
@@ -0,0 +1,20 @@
export const paymentApplicationTableColumns = [
{ prop: 'paymentNo', label: '单据号', minWidth: 140, link: true },
{ prop: 'paymentTypeName', label: '付款类型', minWidth: 110 },
{ prop: 'projectName', label: '所属项目', minWidth: 140 },
{ prop: 'deptName', label: '所属组织', minWidth: 120 },
{ prop: 'contractNo', label: '合同编号', minWidth: 130 },
{ prop: 'contractName', label: '合同名称', minWidth: 140 },
{ prop: 'payerName', label: '付款方', minWidth: 130 },
{ prop: 'payeeName', label: '收款方', minWidth: 130 },
{ prop: 'appliedAmount', label: '申请付款金额', minWidth: 140, money: true },
{ prop: 'paidAmount', label: '已付款金额', minWidth: 130, money: true },
{ prop: 'matchedInvoiceAmount', label: '匹配发票金额', minWidth: 130, money: true },
{ prop: 'applicantName', label: '申请人', minWidth: 100 },
{ prop: 'applyDate', label: '申请日期', minWidth: 120 },
{ prop: 'approvalStatusName', label: '审核状态', minWidth: 110, status: true },
{ prop: 'currentNode', label: '当前节点', minWidth: 120 },
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 120 },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 130 },
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 130 },
];
+17
View File
@@ -0,0 +1,17 @@
export const receiptClaimRecordTableColumns = [
{ prop: 'receiptNoticeNo', label: '认领通知单', minWidth: 150, link: true },
{ prop: 'payerName', label: '付款人', minWidth: 130 },
{ prop: 'receiptAmount', label: '付款金额', minWidth: 130, money: true },
{ prop: 'counterpartyName', label: '对方户名', minWidth: 150 },
{ prop: 'counterpartyAccount', label: '对方账号', minWidth: 170 },
{ prop: 'counterpartyBank', label: '对方开户行', minWidth: 150 },
{ prop: 'summary', label: '摘要', minWidth: 160 },
{ prop: 'transactionTime', label: '交易时间', minWidth: 170 },
{ prop: 'claimAmount', label: '认领金额', minWidth: 130, money: true },
{ prop: 'claimDate', label: '认领日期', minWidth: 120 },
{ prop: 'claimerName', label: '认领人', minWidth: 120 },
{ prop: 'claimerDeptName', label: '所属组织', minWidth: 150 },
{ prop: 'claimStatusName', label: '状态', minWidth: 110, status: true },
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 170 },
{ prop: 'kingdeeBillStatusName', label: '金蝶单据状态', minWidth: 140 },
];
+12
View File
@@ -0,0 +1,12 @@
export const receiptFlowTableColumns = [
{ prop: 'receiptNoticeNo', label: '认领通知单', minWidth: 150 },
{ prop: 'payerName', label: '付款人', minWidth: 130 },
{ prop: 'receiptAmount', label: '付款金额', minWidth: 130, money: true },
{ prop: 'counterpartyName', label: '对方户名', minWidth: 150 },
{ prop: 'counterpartyAccount', label: '对方账号', minWidth: 170 },
{ prop: 'counterpartyBank', label: '对方开户行', minWidth: 150 },
{ prop: 'summary', label: '摘要', minWidth: 160 },
{ prop: 'transactionTime', label: '交易时间', minWidth: 170 },
{ prop: 'claimStatusName', label: '认领状态', minWidth: 110, status: true },
{ prop: 'claimedAmount', label: '已认领金额', minWidth: 130, money: true },
];
+114
View File
@@ -2,6 +2,120 @@ import Layout from '@/page/index/index.vue';
import Store from '@/store/';
export default [
{
path: '/payment/bill-payment/form',
component: Layout,
children: [
{
path: '',
name: '汇票付款表单',
meta: { keepAlive: false, activeMenu: '/payment/bill-payment' },
component: () => import('@/views/payment/bill-payment-form.vue'),
},
],
},
{
path: '/payment/bill-ledger/form',
component: Layout,
children: [
{
path: '',
name: '汇票台账表单',
meta: { keepAlive: false, activeMenu: '/payment/bill-ledger' },
component: () => import('@/views/payment/bill-ledger-form.vue'),
},
],
},
{
path: '/payment/receipt-claim-record/form',
component: Layout,
children: [
{
path: '',
name: '认领记录详情',
meta: { keepAlive: false, activeMenu: '/payment/receipt-claim-record' },
component: () => import('@/views/payment/receipt-claim-record-form.vue'),
},
],
},
{
path: '/payment/receipt-flow/form',
component: Layout,
children: [
{
path: '',
name: '收款流水认领',
meta: { keepAlive: false, activeMenu: '/payment/receipt-flow' },
component: () => import('@/views/payment/receipt-flow-form.vue'),
},
],
},
{
path: '/payment/invoice-receipt/form',
component: Layout,
children: [
{
path: '',
name: '收票登记',
meta: { keepAlive: false, activeMenu: '/payment/invoice-receipt' },
component: () => import('@/views/payment/invoice-receipt-form.vue'),
},
],
},
{
path: '/payment/invoice-application/form',
component: Layout,
children: [
{
path: '',
name: '开票申请',
meta: { keepAlive: false, activeMenu: '/payment/invoice-application' },
component: () => import('@/views/payment/invoice-application-form.vue'),
},
],
},
{
path: '/payment/payment-application/form',
component: Layout,
children: [
{
path: '',
name: '付款申请',
meta: { keepAlive: false, activeMenu: '/payment/payment-application' },
component: () => import('@/views/payment/payment-application-form.vue'),
},
],
},
{
path: '/settlement/pre-settlement/form',
component: Layout,
children: [
{
path: '',
name: '新增编辑预结算',
meta: {
keepAlive: false,
activeMenu: '/settlement/pre-settlement',
},
component: () => import('@/views/settlement/pre-settlement-form.vue'),
},
],
},
{
path: '/settlement/formal-settlement/form',
component: Layout,
children: [
{
path: '',
name: '新增编辑正式结算',
meta: {
keepAlive: false,
activeMenu: '/settlement/formal-settlement',
},
component: () => import('@/views/settlement/formal-settlement-form.vue'),
},
],
},
{
path: '/business/contract-manage/form',
component: Layout,
+25
View File
@@ -0,0 +1,25 @@
const STORAGE_PREFIX = 'settlement-transfer-';
const storageKey = token => `${STORAGE_PREFIX}${token}`;
export const createSettlementTransfer = payload => {
const token = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
window.sessionStorage.setItem(storageKey(token), JSON.stringify(payload));
return token;
};
export const readSettlementTransfer = token => {
if (!token) return null;
const value = window.sessionStorage.getItem(storageKey(token));
if (!value) return null;
try {
return JSON.parse(value);
} catch (error) {
removeSettlementTransfer(token);
return null;
}
};
export const removeSettlementTransfer = token => {
if (token) window.sessionStorage.removeItem(storageKey(token));
};
File diff suppressed because it is too large Load Diff
@@ -131,7 +131,7 @@
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form carrier-form">
<el-row :gutter="16">
<template v-if="isRoad(route)">
<el-col v-if="route.carrierType === '承运商'" :span="6"><el-form-item label="承运商" required><el-select v-model="route.carrierName" filterable remote clearable placeholder="请选择" :remote-method="loadCarrierOptions" :loading="carrierLoading" @visible-change="visible => visible && loadCarrierOptions()"><el-option v-for="item in carrierOptions" :key="carrierOptionKey(item)" :label="carrierOptionLabel(item)" :value="carrierOptionLabel(item)" /></el-select></el-form-item></el-col>
<el-col v-if="route.carrierType !== '自运'" :span="6"><el-form-item label="承运商" required><el-select v-model="route.carrierContractId" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="item.contractId" :label="item.carrierName" :value="item.contractId" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="司机" :required="route.carrierType !== '承运商'"><el-autocomplete :ref="element => setDriverInput(route.segmentNo, element)" v-model="route.driverName" :debounce="300" :fetch-suggestions="fetchDriverSuggestions" clearable placeholder="请输入司机" :loading="driverLoading" @select="item => handleDriverSuggestionSelect(route, item)" /></el-form-item></el-col>
<el-col :span="6"><el-form-item :label="route.carrierType === '承运商' ? '手机号' : '司机手机号'" :required="route.carrierType !== '承运商'"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="车牌号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
@@ -140,7 +140,7 @@
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号" required><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
</template>
<template v-else>
<el-col v-if="route.carrierType === '承运商'" :span="6"><el-form-item label="承运商" required><el-select v-model="route.carrierName" filterable remote clearable placeholder="请选择" :remote-method="loadCarrierOptions" :loading="carrierLoading" @visible-change="visible => visible && loadCarrierOptions()"><el-option v-for="item in carrierOptions" :key="carrierOptionKey(item)" :label="carrierOptionLabel(item)" :value="carrierOptionLabel(item)" /></el-select></el-form-item></el-col>
<el-col v-if="route.carrierType !== '自运'" :span="6"><el-form-item label="承运商" required><el-select v-model="route.carrierContractId" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="item.contractId" :label="item.carrierName" :value="item.contractId" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="船/航/班列号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="船长" required><el-input v-model="route.captainName" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="联系电话" required><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
@@ -180,7 +180,6 @@
<script>
import * as api from '@/api/business/master-order';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { isMobile } from '@/utils/validate';
@@ -251,6 +250,7 @@ export default {
selected: false,
documentType: '运单',
carrierType: '承运商',
carrierContractId: '',
currency: 'CNY',
otherFeeTotal: '',
freightItems: [],
@@ -514,28 +514,29 @@ export default {
route.trailerVehicleNo = '';
route.escortName = '';
route.escortPhone = '';
} else {
}
if (value === '自运') {
route.carrierContractId = '';
route.carrierName = '';
}
},
carrierOptionLabel(item) { return item.customerName || item.carrierName || item.fullName || item.name || ''; },
carrierOptionKey(item) { return item.id || item.customerId || item.carrierId || this.carrierOptionLabel(item); },
handleCarrierContractChange(route, contractId) {
const contract = this.carrierOptions.find(
item => String(item.contractId) === String(contractId)
);
route.carrierContractId = contract?.contractId || '';
route.carrierName = contract?.carrierName || '';
},
driverOptionLabel(item) { return item.driverName || item.name || ''; },
driverOptionKey(item) { return item.id || item.driverId || this.driverOptionLabel(item) || item.mobile; },
handleDriverChange(route, value) {
const driver = this.driverOptions.find(item => this.driverOptionLabel(item) === value);
if (driver) route.driverPhone = driver.mobile || driver.driverPhone || driver.phone || '';
},
async loadCarrierOptions(keyword = '') {
async loadCarrierOptions() {
this.carrierLoading = true;
try {
this.carrierOptions = unwrapRecords(
await getCustomerList(1, 20, {
customerName: keyword,
customerType: '承运商',
status: 1,
})
);
this.carrierOptions = unwrapRecords(await api.getCarriers(this.id));
} finally {
this.carrierLoading = false;
}
@@ -593,9 +594,9 @@ export default {
if (!this.validateRoutePhones(route)) return;
if (route.documentType === '运单') {
if (this.isRoad(route)) {
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo)) return this.$message.warning('请填写承运商车牌号');
if (route.carrierType !== '自运' && (!route.carrierContractId || !route.carrierName || !route.vehicleNo)) return this.$message.warning('请选择承运商并填写车牌号');
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo || !route.trailerVehicleNo || !route.escortName || !route.escortPhone)) return this.$message.warning('请补全自运或网货平台的车辆与人员信息');
} else if (!route.vehicleNo || !route.captainName || !route.driverPhone || !route.containerNo || !route.cabinNo || (route.carrierType === '承运商' && !route.carrierName)) {
} else if (!route.vehicleNo || !route.captainName || !route.driverPhone || !route.containerNo || !route.cabinNo || (route.carrierType !== '自运' && (!route.carrierContractId || !route.carrierName))) {
return this.$message.warning('请补全非公路运输的承运信息');
}
}
@@ -604,7 +605,7 @@ export default {
id: `${route.segmentNo}-${item.cargoName}-${Date.now()}-${Math.random()}`,
batchNo,
segmentNo: route.segmentNo, relationNo: route.segmentNo, documentType: route.documentType, transportType: route.transportType, carrierType: route.carrierType,
carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, captainName: route.captainName, cabinNo: route.cabinNo, containerNo: route.containerNo, trailerVehicleNo: route.trailerVehicleNo, escortName: route.escortName, escortPhone: route.escortPhone, mileage: this.normalizeMileage(route.mileage), unitPrice: this.freightItemsForGoods(route, item).unitPrice || '', priceUnit: this.freightItemsForGoods(route, item).priceUnit || '', currency: route.currency || 'CNY', freightAmount: this.freightAmount(this.freightItemsForGoods(route, item)), freightTotal: this.freightTotal(route), otherFeeTotal: route.otherFeeTotal || '', freightJson: this.buildFreightJson(route), remark: route.remark,
carrierContractId: route.carrierContractId, carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, captainName: route.captainName, cabinNo: route.cabinNo, containerNo: route.containerNo, trailerVehicleNo: route.trailerVehicleNo, escortName: route.escortName, escortPhone: route.escortPhone, mileage: this.normalizeMileage(route.mileage), unitPrice: this.freightItemsForGoods(route, item).unitPrice || '', priceUnit: this.freightItemsForGoods(route, item).priceUnit || '', currency: route.currency || 'CNY', freightAmount: this.freightAmount(this.freightItemsForGoods(route, item)), freightTotal: this.freightTotal(route), otherFeeTotal: route.otherFeeTotal || '', freightJson: this.buildFreightJson(route), remark: route.remark,
departureName: route.departureName, departureAddress: route.departureAddress, departureContact: route.departureContact, departurePhone: route.departurePhone,
arrivalName: route.arrivalName, arrivalAddress: route.arrivalAddress, arrivalContact: route.arrivalContact, arrivalPhone: route.arrivalPhone,
estimatedStartTime: route.estimatedStartTime, estimatedEndTime: route.estimatedEndTime,
@@ -853,6 +853,10 @@ export default {
}
return String(value).replace('T', ' ').slice(0, 10);
},
normalizeDateTimeValue(value, endOfDay = false) {
const date = this.normalizeDateValue(value);
return date ? `${date} ${endOfDay ? '23:59:59' : '00:00:00'}` : '';
},
getRegionDisplay(target, fallback) {
const labels = this.regionLabels(this.addressRegionPaths[target] || []);
return labels.length ? labels.join('/') : fallback;
@@ -1689,8 +1693,8 @@ export default {
}
const payload = {
...this.form,
planStartTime: this.normalizeDateValue(this.form.planStartTime),
planEndTime: this.normalizeDateValue(this.form.planEndTime),
planStartTime: this.normalizeDateTimeValue(this.form.planStartTime),
planEndTime: this.normalizeDateTimeValue(this.form.planEndTime, true),
attachmentsJson: JSON.stringify(this.attachmentRows),
routes: [
...this.form.routes.map((route, index) => ({ ...route, segmentNo: `段${index + 1}` })),
File diff suppressed because it is too large Load Diff
+99 -19
View File
@@ -822,23 +822,26 @@
/>
</div>
<div class="loading-manage-dialog__grid loading-manage-dialog__task-grid">
<el-form-item v-if="!['自运', '网货平台'].includes(dialogForm.carrierType)" label="承运商" required>
<el-form-item
v-if="dialogForm.carrierType !== '自运'"
label="承运商"
required
>
<el-select
v-model="dialogForm.carrierName"
v-model="dialogForm.carrierContractId"
clearable
filterable
remote
reserve-keyword
:remote-method="loadCarrierOptions"
:loading="carrierLoading"
placeholder="请输入"
@visible-change="visible => visible && loadCarrierOptions(dialogForm.carrierName)"
:disabled="dialogReadonly || !selectedWaybillRows.length"
:loading="taskCarrierLoading"
:placeholder="taskCarrierPlaceholder"
@change="handleTaskCarrierChange"
@visible-change="visible => visible && loadTaskCarrierOptions()"
>
<el-option
v-for="item in carrierOptions"
:key="item.id || item.fullName || item.customerName || item.carrierName"
:label="item.fullName || item.customerName || item.carrierName || item.name"
:value="item.fullName || item.customerName || item.carrierName || item.name"
v-for="item in taskCarrierOptions"
:key="item.id"
:label="item.carrierName"
:value="item.id"
/>
</el-select>
</el-form-item>
@@ -1054,6 +1057,7 @@ const createDialogForm = () => ({
driverPhone: '',
carrierType: '承运商',
carrierName: '',
carrierContractId: '',
escortName: '',
escortPhone: '',
departureAddress: '',
@@ -1165,13 +1169,16 @@ export default {
cargoTypeOptions: [],
driverOptions: [],
carrierOptions: [],
taskCarrierOptions: [],
planOptions: [],
projectLoading: false,
customerLoading: false,
cargoTypeLoading: false,
driverLoading: false,
carrierLoading: false,
taskCarrierLoading: false,
planLoading: false,
taskCarrierRequestId: 0,
carrierTypeOptions,
statusOptions: loadingStatusOptions,
dataSourceOptions: loadingManageConfig.dataSourceOptions,
@@ -1199,6 +1206,11 @@ export default {
dialogReadonly() {
return this.dialogMode === 'view';
},
taskCarrierPlaceholder() {
if (!this.selectedWaybillRows.length) return '请先选择运单';
if (!this.taskCarrierLoading && !this.taskCarrierOptions.length) return '暂无可用承运商合同';
return '请选择';
},
dialogTitle() {
const titleMap = {
add: '新建配载',
@@ -1567,6 +1579,10 @@ export default {
this.dialogForm.waybillIdsJson,
this.dialogForm.loadingSubNos
);
if (this.dialogForm.carrierType === '自运') {
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = '';
}
this.syncProcessProjectOptions();
this.restoreRouteAndCargo();
this.restoreRouteChangeRecords();
@@ -1604,6 +1620,9 @@ export default {
this.candidateRows = [];
this.candidateSelection = [];
this.selectedWaybillRows = [];
this.taskCarrierRequestId += 1;
this.taskCarrierOptions = [];
this.taskCarrierLoading = false;
this.routeNodes = [];
this.cargoRows = [];
this.candidatePage.currentPage = 1;
@@ -1855,10 +1874,12 @@ export default {
);
this.selectedWaybillRows = rows.filter(Boolean);
this.rebuildSummaryFromWaybills(false);
await this.loadTaskCarrierOptions();
return;
}
this.selectedWaybillRows = this.splitText(loadingSubNos).map(waybillNo => ({ waybillNo }));
this.rebuildSummaryFromWaybills(false);
await this.loadTaskCarrierOptions();
},
async loadCandidateWaybills() {
this.candidateLoading = true;
@@ -1933,6 +1954,7 @@ export default {
});
this.selectedWaybillRows = nextRows;
this.rebuildSummaryFromWaybills();
await this.loadTaskCarrierOptions();
this.$refs.candidateTableRef?.clearSelection?.();
this.candidateSelection = [];
} finally {
@@ -1942,6 +1964,7 @@ export default {
removeSelectedWaybill(index) {
this.selectedWaybillRows.splice(index, 1);
this.rebuildSummaryFromWaybills();
this.loadTaskCarrierOptions();
},
rebuildSummaryFromWaybills(rebuildRoute = true) {
const rows = this.selectedWaybillRows;
@@ -2071,6 +2094,10 @@ export default {
normalizePayload() {
this.syncRouteAddressFields();
this.rebuildSummaryFromWaybills(false);
if (this.dialogForm.carrierType === '自运') {
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = '';
}
return {
...this.dialogForm,
routeJson: JSON.stringify(this.routeNodes),
@@ -2130,14 +2157,15 @@ export default {
ElMessage.warning('请输入车牌号');
return false;
}
if (this.dialogForm.carrierType !== '自运' && !this.dialogForm.carrierContractId) {
ElMessage.warning('请选择承运商合同');
return false;
}
if (['自运', '网货平台'].includes(this.dialogForm.carrierType)) {
if (!this.dialogForm.driverName || !this.dialogForm.driverPhone) {
ElMessage.warning('请输入司机和司机手机号');
return false;
}
} else if (!this.dialogForm.carrierName) {
ElMessage.warning('请选择承运商');
return false;
}
return true;
},
@@ -2217,9 +2245,14 @@ export default {
const ids = this.selectionRows.map(row => row.id).join(',');
const res = await loadingApi.batchComplete(ids);
const result = res.data?.data || {};
ElMessage.success(
`批量完成成功 ${result.successCount || 0} 条,跳过 ${result.skippedCount || 0} 条`
);
const summary = `批量完成成功 ${result.successCount || 0} 条,跳过 ${
result.skippedCount || 0
} 条`;
if (result.skippedCount && result.skippedReasons?.length) {
ElMessage.warning(`${summary}${result.skippedReasons.join('')}`);
} else {
ElMessage.success(summary);
}
this.loadTable();
})
.catch(() => {});
@@ -2245,10 +2278,17 @@ export default {
this.dialogForm.trailerVehicleNo = '';
this.dialogForm.escortName = '';
this.dialogForm.escortPhone = '';
} else if (['自运', '网货平台'].includes(this.dialogForm.carrierType)) {
}
if (this.dialogForm.carrierType === '自运') {
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = '';
}
},
handleTaskCarrierChange(contractId) {
const contract = this.taskCarrierOptions.find(item => String(item.id) === String(contractId));
this.dialogForm.carrierContractId = contract?.id || '';
this.dialogForm.carrierName = contract?.carrierName || '';
},
handleDriverChange(value) {
const driver = this.driverOptions.find(item => (item.driverName || item.name) === value);
if (driver && !this.dialogForm.driverPhone) {
@@ -2343,6 +2383,46 @@ export default {
this.carrierLoading = false;
}
},
async loadTaskCarrierOptions() {
const requestId = ++this.taskCarrierRequestId;
if (!this.selectedWaybillRows.length) {
this.taskCarrierOptions = [];
this.taskCarrierLoading = false;
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = '';
return [];
}
this.taskCarrierLoading = true;
try {
const response = await loadingApi.getCarrierContracts();
if (requestId !== this.taskCarrierRequestId) return [];
const contracts = response.data?.data || response.data || [];
this.taskCarrierOptions = contracts;
const current =
contracts.find(
item => String(item.id) === String(this.dialogForm.carrierContractId)
) ||
contracts.find(
item => item.carrierName === this.dialogForm.carrierName
);
if (current) {
this.dialogForm.carrierContractId = current.id;
this.dialogForm.carrierName = current.carrierName;
} else {
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = '';
}
return contracts;
} catch (error) {
if (requestId !== this.taskCarrierRequestId) return [];
this.taskCarrierOptions = [];
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = '';
return [];
} finally {
if (requestId === this.taskCarrierRequestId) this.taskCarrierLoading = false;
}
},
async loadPlanOptions(keyword = '') {
this.planLoading = true;
try {
+653 -4
View File
@@ -1,20 +1,669 @@
<template>
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="320" />
<basic-container class="temporary-credit-limit-page">
<avue-crud
ref="crud"
v-model="form"
v-model:page="page"
:data="data"
:option="tableOption"
:permission="permissionList"
:table-loading="loading"
:before-open="beforeOpen"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button v-if="canCreate" type="primary" icon="el-icon-plus" @click="$refs.crud.rowAdd()">
新增
</el-button>
<el-button
v-if="config.exportUrl && hasPermission(`${config.permission}_export`)"
type="primary"
icon="el-icon-download"
plain
@click="handleExport"
>
批量导出
</el-button>
<el-button
v-if="hasPermission(`${config.permission}_delete`)"
type="danger"
icon="el-icon-delete"
plain
@click="handleDelete"
>
批量删除
</el-button>
</template>
<template #approvalStatus="{ row }">
<el-tag :type="statusTagType(row.approvalStatus)">
{{ displayStatus(row, 'approvalStatus') }}
</el-tag>
</template>
<template #projectName-form>
<el-select
v-model="selectedProjectId"
class="temporary-credit-limit-page__field"
placeholder="请选择项目"
filterable
clearable
:loading="projectLoading"
:disabled="dialogReadonly"
@visible-change="visible => visible && loadProjectOptions()"
@change="handleProjectChange"
>
<el-option
v-for="item in projectOptions"
:key="item.id"
:label="item.projectName"
:value="item.id"
/>
</el-select>
</template>
<template #applyLimit-form>
<el-input
v-model="form.applyLimit"
class="temporary-credit-limit-page__field"
placeholder="请输入申请临时额度"
:disabled="dialogReadonly"
@input="handleAmountInput"
/>
</template>
<template #attachmentsJson-form>
<div class="temporary-credit-limit-page__attachment">
<div class="temporary-credit-limit-page__attachment-head">
<vehicle-attachment-upload
v-model="attachmentRows"
:readonly="dialogReadonly"
:file-types="attachmentFileTypes"
:max-size="50"
:show-file-list="false"
button-text="上传附件"
@change="handleAttachmentChange"
/>
<el-button type="primary" :disabled="!attachmentRows.length" @click="batchDownload">
批量下载
</el-button>
</div>
<el-table :data="attachmentRows" border @selection-change="selectedAttachments = $event">
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="240" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="downloadAttachment(row)">
{{ attachmentName(row) }}
</el-link>
</template>
</el-table-column>
<el-table-column label="文件大小" width="120" align="center">
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
<el-table-column prop="uploadTime" label="上传时间" width="180" align="center" />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="{ row, $index }">
<el-link v-if="!dialogReadonly" type="danger" @click="removeAttachment($index)">
删除
</el-link>
<el-link v-else type="primary" @click="downloadAttachment(row)">下载</el-link>
</template>
</el-table-column>
</el-table>
</div>
</template>
<template #menu-form-before>
<el-button
v-if="!dialogReadonly"
type="primary"
plain
:loading="draftLoading"
@click="saveDraft"
>
保存
</el-button>
</template>
<template #menu="{ row }">
<el-link
v-if="hasPermission(`${config.permission}_view`)"
type="primary"
@click="$refs.crud.rowView(row)"
>
查看
</el-link>
<el-link v-if="canEdit(row)" type="primary" @click="$refs.crud.rowEdit(row)">
编辑
</el-link>
<el-link
v-for="operation in customOperations(row)"
:key="operation.action"
:type="operation.type || 'primary'"
@click="handleOperation(operation, row)"
>
{{ operation.label }}
</el-link>
<el-link v-if="canDelete(row)" type="danger" @click="rowDel(row)">删除</el-link>
</template>
</avue-crud>
<empty-pagination
:page="page"
@size-change="sizeChange"
@current-change="currentChange"
@load="onLoad(page, query)"
/>
<flow-design-step
v-if="website.design.designMode"
v-model:is-display="flowBox"
:process-instance-id="processInstanceId"
/>
<el-dialog v-else v-model="flowBox" title="流程图" append-to-body fullscreen>
<iframe class="temporary-credit-limit-page__flow" :src="flowUrl" />
<template #footer><el-button @click="flowBox = false">关闭</el-button></template>
</el-dialog>
</basic-container>
</template>
<script>
import BusinessCrudPage from './components/business-crud-page.vue';
import { exportBlob } from '@/api/common';
import * as api from '@/api/business/temporary-credit-limit';
import {
getDetail as getProjectDetail,
getList as getProjectList,
} from '@/api/business/project-apply';
import { config, option } from '@/option/business/temporary-credit-limit';
import { getToken } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const attachmentFileTypes = [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
];
const extractRecords = res => {
const data = res?.data?.data || res?.data || {};
return Array.isArray(data) ? data : data.records || [];
};
export default {
components: { BusinessCrudPage },
name: 'TemporaryCreditLimit',
data() {
return {
api,
config,
option,
form: {},
data: [],
query: {},
loading: true,
draftLoading: false,
dialogReadonly: false,
tableOption: this.buildTableOption(),
page: {
currentPage: 1,
pageSize: 10,
pageSizes: [10, 20, 50, 100],
total: 0,
},
selectionList: [],
selectedProjectId: '',
projectOptions: [],
projectLoading: false,
attachmentRows: [],
selectedAttachments: [],
attachmentFileTypes,
flowBox: false,
flowUrl: '',
processInstanceId: '',
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
permissionList() {
return { addBtn: this.canCreate };
},
isAdmin() {
const authority = this.userInfo?.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
},
canCreate() {
return this.hasPermission(`${this.config.permission}_add`);
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
},
created() {
this.loadProjectOptions();
},
methods: {
buildTableOption() {
return {
...option,
addBtn: false,
viewBtn: false,
editBtn: false,
delBtn: false,
menuWidth: 320,
column: (option.column || []).map(column => ({ ...column })),
};
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission?.[code], false);
},
statusValue(row) {
return row[this.config.statusProp || 'status'];
},
statusTagType(status) {
if (['approved', 'change_approved'].includes(status)) return 'success';
if (['draft', 'withdrawn'].includes(status)) return 'info';
if (['rejected', 'change_rejected'].includes(status)) return 'danger';
return 'warning';
},
displayStatus(row, prop) {
const textProp = prop === this.config.statusProp ? this.config.statusTextProp : '';
if (textProp && row[textProp]) return row[textProp];
const column = this.findColumn(this.tableOption.column, prop);
const item = column?.dicData?.find(dic => String(dic.value) === String(row[prop]));
return item?.label || row[prop] || '未知';
},
canEdit(row) {
return (
this.hasPermission(`${this.config.permission}_edit`) &&
!row.readonly &&
(this.config.editStatus || []).includes(this.statusValue(row))
);
},
canDelete(row) {
return (
this.hasPermission(`${this.config.permission}_delete`) &&
!row.readonly &&
(this.config.deleteStatus || []).includes(this.statusValue(row))
);
},
customOperations(row) {
return (this.config.operations || []).filter(operation => {
const permission = operation.permission || `${this.config.permission}_${operation.action}`;
const statusProp = operation.statusProp || this.config.statusProp || 'status';
return (
this.hasPermission(permission) &&
!row.readonly &&
(!operation.status?.length || operation.status.includes(row[statusProp]))
);
});
},
onLoad(page, params = {}) {
this.loading = true;
this.api
.getList(page.currentPage, page.pageSize, { ...params, ...this.query })
.then(res => {
const result = res.data?.data || {};
this.page.total = result.total || 0;
this.data = result.records || [];
this.selectionClear();
})
.finally(() => {
this.loading = false;
});
},
searchChange(params, done) {
this.query = { ...params };
this.page.currentPage = 1;
this.onLoad(this.page);
done();
},
searchReset() {
this.query = {};
this.page.currentPage = 1;
this.onLoad(this.page);
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud?.toggleSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
beforeOpen(done, type) {
this.dialogReadonly = type === 'view';
if (type === 'add') {
this.form = { ...(this.config.defaultForm || {}) };
this.selectedProjectId = '';
this.attachmentRows = [];
this.selectedAttachments = [];
done();
return;
}
this.api
.getDetail(this.form.id)
.then(res => this.applyDetail(res.data?.data || {}))
.finally(done);
},
applyDetail(detail) {
this.form = { ...detail };
this.selectedProjectId = detail.projectId || '';
this.attachmentRows = this.parseJsonArray(detail.attachmentsJson);
this.selectedAttachments = [];
this.syncCurrentProjectOption();
},
normalizeForm(row = this.form) {
return {
...row,
applyLimit: row.applyLimit === '' ? '' : Number(row.applyLimit),
attachmentsJson: JSON.stringify(this.attachmentRows),
};
},
rowSave(row, done, loading) {
this.api
.submit(this.normalizeForm(row))
.then(() => {
this.$message.success('新增成功');
done();
this.onLoad(this.page, this.query);
})
.catch(() => loading());
},
rowUpdate(row, index, done, loading) {
this.api
.submit(this.normalizeForm(row))
.then(() => {
this.$message.success('修改成功');
done();
this.onLoad(this.page, this.query);
})
.catch(() => loading());
},
saveDraft() {
if (this.draftLoading) return;
this.draftLoading = true;
this.api
.saveDraft(this.normalizeForm())
.then(() => {
this.$message.success('保存成功');
this.$refs.crud?.closeDialog();
this.onLoad(this.page, this.query);
})
.finally(() => {
this.draftLoading = false;
});
},
rowDel(row) {
this.removeRows([row]);
},
handleDelete() {
if (!this.selectionList.length) {
this.$message.warning('请选择至少一条数据');
return;
}
const rows = this.selectionList.filter(this.canDelete);
if (!rows.length) {
this.$message.warning('所选数据当前状态不可删除');
return;
}
this.removeRows(rows);
},
removeRows(rows) {
this.$confirm('确定将选择数据删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => this.api.remove(rows.map(row => row.id).join(',')))
.then(() => {
this.$message.success('删除成功');
this.onLoad(this.page, this.query);
});
},
handleOperation(operation, row) {
if (operation.action === 'flow') {
this.openFlow(row);
return;
}
this.$confirm(`确定${operation.label}该${this.config.title}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => this.api[operation.action](row.id))
.then(() => {
this.$message.success(`${operation.label}成功`);
this.onLoad(this.page, this.query);
});
},
openFlow(row, loaded = false) {
const processInstanceId = row.processInstanceId || row.processInstanceID || row.procInstId;
if (!processInstanceId && !loaded && row.id) {
this.api.getDetail(row.id).then(res => this.openFlow(res.data?.data || {}, true));
return;
}
if (!processInstanceId) {
this.$message.warning('流程实例为空,无法查看流程');
return;
}
if (this.website.design.designMode) this.processInstanceId = processInstanceId;
else
this.flowUrl = `/api/blade-flow/process/diagram-view?processInstanceId=${processInstanceId}`;
this.flowBox = true;
},
loadProjectOptions() {
if (this.projectLoading) return;
this.projectLoading = true;
getProjectList(1, 9999, this.config.projectQueryParams || {})
.then(res => {
this.projectOptions = extractRecords(res);
this.syncCurrentProjectOption();
})
.finally(() => {
this.projectLoading = false;
});
},
syncCurrentProjectOption() {
if (!this.form.projectId || !this.form.projectName) return;
if (this.projectOptions.some(item => String(item.id) === String(this.form.projectId))) return;
this.projectOptions.unshift({
id: this.form.projectId,
projectName: this.form.projectName,
projectCode: this.form.projectCode,
undertakeDeptName: this.form.undertakeDeptName,
fundLimit: this.form.projectFundLimit,
});
},
handleProjectChange(projectId) {
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
if (!project) {
Object.assign(this.form, {
projectId: '',
projectName: '',
projectCode: '',
undertakeDeptName: '',
projectFundLimit: '',
usedFundLimit: 0,
remainingFundLimit: 0,
});
return;
}
Object.assign(this.form, {
projectId: project.id,
projectName: project.projectName || '',
projectCode: project.projectCode || '',
undertakeDeptName: project.undertakeDeptName || '',
projectFundLimit: project.fundLimit || project.projectFundLimit || 0,
usedFundLimit: project.usedFundLimit || 0,
});
this.form.remainingFundLimit = this.calculateRemainingFundLimit();
getProjectDetail(project.id).then(res => {
const detail = res.data?.data || {};
Object.assign(this.form, {
projectCode: detail.projectCode || this.form.projectCode,
undertakeDeptName: detail.undertakeDeptName || this.form.undertakeDeptName,
projectFundLimit:
detail.fundLimit ?? detail.projectFundLimit ?? this.form.projectFundLimit,
usedFundLimit: detail.usedFundLimit ?? this.form.usedFundLimit,
});
this.form.remainingFundLimit = this.calculateRemainingFundLimit();
});
},
calculateRemainingFundLimit() {
const total = Number(this.form.projectFundLimit);
const used = Number(this.form.usedFundLimit);
if (!Number.isFinite(total) || !Number.isFinite(used)) return '';
return Math.round((total - used + Number.EPSILON) * 100) / 100;
},
handleAmountInput(value) {
const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.');
this.form.applyLimit =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
},
handleAttachmentChange(list) {
const uploadUserName = this.userInfo?.realName || this.userInfo?.userName || '';
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.attachmentRows = (list || []).map(item => ({
...item,
uploadUserName: item.uploadUserName || uploadUserName,
uploadTime: item.uploadTime || uploadTime,
}));
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
},
removeAttachment(index) {
this.attachmentRows.splice(index, 1);
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
},
parseJsonArray(value) {
if (Array.isArray(value)) return value;
if (!value) return [];
try {
const data = JSON.parse(value);
return Array.isArray(data) ? data : [];
} catch (error) {
return [];
}
},
attachmentName(row = {}) {
return row.originalName || row.name || row.fileName || '附件';
},
attachmentUrl(row = {}) {
return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || '';
},
downloadAttachment(row) {
const url = this.attachmentUrl(row);
if (!url) {
this.$message.warning('附件地址为空,无法下载');
return;
}
downloadFileByUrl(url, this.attachmentName(row));
},
batchDownload() {
const rows = this.selectedAttachments.length ? this.selectedAttachments : this.attachmentRows;
rows.forEach(this.downloadAttachment);
},
formatFileSize(size) {
const value = Number(size);
if (!value) return '';
if (value < 1024) return `${value}B`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)}KB`;
return `${(value / 1024 / 1024).toFixed(1)}MB`;
},
handleExport() {
this.$confirm(`是否导出${this.config.exportName || this.config.title}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(
this.config.exportUrl,
{ ...this.query, ids: this.ids, [this.website.tokenHeader]: getToken() },
{ feedback: true }
)
.then(res => {
downloadXls(
res.data,
`${this.config.exportName || this.config.title}${this.$dayjs().format(
'YYYY-MM-DD HH:mm:ss'
)}.xlsx`
);
})
.finally(() => NProgress.done());
});
},
},
};
</script>
<style lang="scss" scoped>
.temporary-credit-limit-page {
&__field {
width: 100%;
}
&__attachment-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
&__flow {
width: 100%;
height: calc(100vh - 150px);
border: 0;
}
:deep(.avue-crud__header) {
margin-top: 12px;
}
:deep(.avue-crud__menu) {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
</style>
<style>
.temporary-credit-limit-dialog .el-form-item__label {
white-space: normal;
line-height: 1.2;
}
</style>
+599
View File
@@ -0,0 +1,599 @@
<template>
<basic-container class="bill-ledger-form-page">
<div class="bill-ledger-form-page__title">
{{ recordId ? '编辑汇票台账' : '新增汇票台账' }}
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="right"
label-width="auto"
class="bill-ledger-form-page__form"
>
<section class="bill-ledger-form-page__section">
<div class="bill-ledger-form-page__section-title">基本信息</div>
<el-row :gutter="48">
<el-col :span="12">
<el-form-item label="票据号码" prop="billNo">
<el-input
v-model="form.billNo"
:disabled="Boolean(recordId)"
maxlength="32"
placeholder="请输入"
/>
</el-form-item>
<el-form-item label="出票单位" prop="issuerId">
<el-select
v-model="form.issuerId"
filterable
remote
reserve-keyword
:remote-method="searchCustomers"
:loading="customerLoading"
placeholder="请选择客商基础档案"
>
<el-option
v-for="item in customerOptions"
:key="item.id"
:label="customerName(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="票面金额" prop="faceAmount">
<el-input-number
v-model="form.faceAmount"
:min="0"
:precision="2"
:controls="false"
placeholder="请输入"
/>
</el-form-item>
<el-form-item label="出票日期" prop="issueDate">
<el-date-picker
v-model="form.issueDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择"
/>
</el-form-item>
<el-form-item label="可用部门" prop="availableDeptIds">
<el-tree-select
v-model="form.availableDeptIds"
:data="deptTree"
:props="deptProps"
node-key="id"
multiple
show-checkbox
collapse-tags
collapse-tags-tooltip
placeholder="请选择,默认全部"
@change="handleDeptChange"
/>
</el-form-item>
<el-form-item label="费用承担方" prop="feeBearerId">
<el-select
v-model="form.feeBearerId"
filterable
remote
reserve-keyword
:remote-method="searchCustomers"
:loading="customerLoading"
placeholder="请选择客商基础档案"
>
<el-option
v-for="item in customerOptions"
:key="item.id"
:label="customerName(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="双方确认贴现率" prop="confirmedDiscountRate">
<el-input-number
v-model="form.confirmedDiscountRate"
:min="0"
:max="100"
:precision="4"
:controls="false"
placeholder="请输入"
>
<template #suffix>%</template>
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="汇票类型" prop="billType">
<el-select v-model="form.billType" placeholder="请选择">
<el-option
v-for="item in billTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="收票单位" prop="receiverName">
<el-select
v-model="form.receiverName"
filterable
allow-create
default-first-option
placeholder="请选择或输入"
>
<el-option
v-for="item in receiverOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="可用余额">
<el-input :model-value="formatMoney(projectedAvailableBalance)" disabled />
</el-form-item>
<el-form-item label="到期日期" prop="maturityDate">
<el-date-picker
v-model="form.maturityDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择"
/>
</el-form-item>
<el-form-item label="出票行" prop="issuingBank">
<el-select
v-model="form.issuingBank"
filterable
allow-create
default-first-option
placeholder="请选择或输入"
>
<el-option v-for="item in bankOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="银行贴现参考率" prop="bankDiscountReferenceRate">
<el-input-number
v-model="form.bankDiscountReferenceRate"
:min="0"
:max="100"
:precision="4"
:controls="false"
placeholder="请输入"
>
<template #suffix>%</template>
</el-input-number>
</el-form-item>
<el-form-item label="预计贴现费用">
<el-input :model-value="formatMoney(estimatedDiscountFee)" disabled />
</el-form-item>
</el-col>
</el-row>
</section>
<section class="bill-ledger-form-page__section">
<div class="bill-ledger-form-page__section-title">附件信息</div>
<el-table :data="form.attachments" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="文件名" min-width="220">
<template #default="{ row }">
<el-link
v-if="row.url || row.link"
type="primary"
:href="row.url || row.link"
target="_blank"
>
{{ row.originalName || row.name || '附件' }}
</el-link>
<span v-else>{{ row.originalName || row.name || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="240">
<template #default="{ row }">
<el-input v-model="row.description" maxlength="200" placeholder="请输入" />
</template>
</el-table-column>
<el-table-column label="文件大小" width="120" align="center">
<template #default="{ row }">{{ fileSize(row) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" width="130" align="center" />
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" />
<el-table-column label="操作" width="100" align="center">
<template #default="{ $index }">
<el-link type="danger" @click="form.attachments.splice($index, 1)">删除</el-link>
</template>
</el-table-column>
</el-table>
<vehicle-attachment-upload
v-model="form.attachments"
class="bill-ledger-form-page__uploader"
:multiple="true"
:limit="20"
:max-size="10"
:file-types="attachmentFileTypes"
:show-file-list="false"
button-text="上传"
@change="normalizeAttachments"
/>
</section>
<section class="bill-ledger-form-page__section">
<div class="bill-ledger-form-page__section-title">备注</div>
<el-form-item prop="remark" class="bill-ledger-form-page__remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="4"
maxlength="200"
show-word-limit
placeholder="请输入"
/>
</el-form-item>
</section>
<section class="bill-ledger-form-page__section">
<div class="bill-ledger-form-page__section-title">使用记录</div>
<el-table :data="usageRecords" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="applicationNo" label="申请单号" min-width="180" align="center" />
<el-table-column label="使用金额" min-width="150" align="center">
<template #default="{ row }">{{ formatMoney(row.usedAmount) }}</template>
</el-table-column>
<el-table-column prop="useDeptName" label="使用部门" min-width="180" align="center" />
<el-table-column label="状态" min-width="120" align="center">
<template #default>已审核</template>
</el-table-column>
</el-table>
</section>
<div class="bill-ledger-form-page__actions">
<el-button type="primary" :loading="saving" @click="confirmSubmit">确认</el-button>
<el-button @click="goBack">取消</el-button>
</div>
</el-form>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/billLedger';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getDeptTree } from '@/api/system/dept';
const emptyForm = () => ({
id: null,
billNo: '',
issuerId: null,
receiverName: '',
billType: 'issued',
faceAmount: null,
availableBalance: 0,
issueDate: '',
maturityDate: '',
availableDeptIds: ['all'],
availableDeptNames: '全部',
feeBearerId: null,
confirmedDiscountRate: null,
issuingBank: '',
bankDiscountReferenceRate: null,
attachments: [],
remark: '',
});
export default {
name: 'BillLedgerForm',
data() {
return {
form: emptyForm(),
usageRecords: [],
billTypeOptions: api.billTypeOptions,
customerOptions: [],
receiverOptions: [],
customerLoading: false,
saving: false,
deptTree: [],
deptMap: new Map(),
deptProps: { label: 'title', value: 'id', children: 'children' },
bankOptions: [
'中国工商银行',
'中国农业银行',
'中国银行',
'中国建设银行',
'交通银行',
'招商银行',
'浦发银行',
'中信银行',
],
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
],
rules: {
billNo: [
{ required: true, message: '请输入票据号码', trigger: 'blur' },
{ max: 32, message: '票据号码不能超过32个字符', trigger: 'blur' },
],
issuerId: [{ required: true, message: '请选择出票单位', trigger: 'change' }],
receiverName: [{ required: true, message: '请选择或输入收票单位', trigger: 'change' }],
billType: [{ required: true, message: '请选择汇票类型', trigger: 'change' }],
faceAmount: [{ validator: this.validateFaceAmount, trigger: 'change' }],
issueDate: [{ required: true, message: '请选择出票日期', trigger: 'change' }],
maturityDate: [{ validator: this.validateMaturityDate, trigger: 'change' }],
availableDeptIds: [{ validator: this.validateDepartments, trigger: 'change' }],
feeBearerId: [{ required: true, message: '请选择费用承担方', trigger: 'change' }],
confirmedDiscountRate: [{ validator: this.validateRate, trigger: 'change' }],
issuingBank: [
{ required: true, message: '请选择或输入出票行', trigger: 'change' },
{ max: 100, message: '出票行不能超过100个字符', trigger: 'change' },
],
bankDiscountReferenceRate: [{ validator: this.validateRate, trigger: 'change' }],
remark: [{ max: 200, message: '备注不能超过200个字符', trigger: 'blur' }],
},
};
},
computed: {
...mapGetters(['userInfo']),
recordId() {
return this.$route.query.id || '';
},
activeUsedAmount() {
return this.usageRecords.reduce((total, item) => total + Number(item.usedAmount || 0), 0);
},
projectedAvailableBalance() {
return Math.max(Number(this.form.faceAmount || 0) - this.activeUsedAmount, 0);
},
estimatedDiscountFee() {
return (
(Number(this.form.faceAmount || 0) * Number(this.form.confirmedDiscountRate || 0)) / 100
);
},
},
created() {
this.initialize();
},
methods: {
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async initialize() {
await Promise.all([this.loadDepartments(), this.searchCustomers('')]);
if (!this.recordId) {
this.form.issueDate = this.$dayjs().format('YYYY-MM-DD');
return;
}
const data = this.unwrapData(await api.getDetail(this.recordId));
this.form = {
...emptyForm(),
...data,
availableDeptIds: this.parse(data.availableDeptIdsJson),
attachments: this.parse(data.attachmentsJson),
};
this.usageRecords = data.usageRecords || [];
this.receiverOptions = data.receiverName ? [data.receiverName] : [];
this.ensureCustomerOption(data.issuerId, data.issuerName);
this.ensureCustomerOption(data.feeBearerId, data.feeBearerName);
this.handleDeptChange(this.form.availableDeptIds);
},
parse(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
try {
return JSON.parse(value) || [];
} catch {
return [];
}
},
async loadDepartments() {
const response = await getDeptTree(this.userInfo?.tenantId);
const rows = this.unwrapData(response) || [];
this.deptTree = [{ id: 'all', title: '全部', children: rows }];
this.deptMap = new Map([['all', '全部']]);
this.flattenDepartments(rows).forEach(item => this.deptMap.set(String(item.id), item.title));
},
flattenDepartments(rows) {
return (rows || []).flatMap(item => [item, ...this.flattenDepartments(item.children || [])]);
},
handleDeptChange(values) {
const ids = values || [];
this.form.availableDeptNames = ids.some(item => String(item) === 'all')
? '全部'
: ids
.map(item => this.deptMap.get(String(item)))
.filter(Boolean)
.join('、');
},
async searchCustomers(keyword) {
this.customerLoading = true;
try {
const data = this.unwrapData(
await getCustomerList(1, 100, { approvalStatus: 'approved', fullName: keyword || '' })
);
const current = this.customerOptions.filter(item =>
[this.form.issuerId, this.form.feeBearerId].some(id => String(id) === String(item.id))
);
this.customerOptions = [...current, ...(data.records || [])].filter(
(item, index, rows) => rows.findIndex(row => String(row.id) === String(item.id)) === index
);
this.receiverOptions = [
...new Set(this.customerOptions.map(item => this.customerName(item)).filter(Boolean)),
];
} finally {
this.customerLoading = false;
}
},
ensureCustomerOption(id, name) {
if (!id || this.customerOptions.some(item => String(item.id) === String(id))) return;
this.customerOptions.push({ id, fullName: name, shortName: name });
},
customerName(item) {
return item.fullName || item.shortName || item.customerCode || '-';
},
validateFaceAmount(rule, value, callback) {
if (!Number.isFinite(Number(value)) || Number(value) <= 0) {
callback(new Error('票面金额必须大于0'));
return;
}
if (Number(value) < this.activeUsedAmount) {
callback(new Error('票面金额不能小于已使用金额'));
return;
}
callback();
},
validateMaturityDate(rule, value, callback) {
if (!value) {
callback(new Error('请选择到期日期'));
return;
}
if (this.form.issueDate && !this.$dayjs(value).isAfter(this.$dayjs(this.form.issueDate))) {
callback(new Error('到期日期必须晚于出票日期'));
return;
}
callback();
},
validateDepartments(rule, value, callback) {
if (!Array.isArray(value) || !value.length) {
callback(new Error('请选择可用部门'));
return;
}
callback();
},
validateRate(rule, value, callback) {
if (value !== null && value !== undefined && value !== '') {
const rate = Number(value);
if (!Number.isFinite(rate) || rate < 0 || rate > 100) {
callback(new Error('贴现率必须在0-100之间'));
return;
}
}
callback();
},
normalizeAttachments(files) {
const userName = this.userInfo?.realName || this.userInfo?.userName || '';
const time = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.form.attachments = (files || []).map(file => ({
...file,
description: file.description || '',
uploadUserName: file.uploadUserName || userName,
uploadTime: file.uploadTime || time,
}));
},
fileSize(file) {
if (typeof file.size === 'string' && /[a-z]/i.test(file.size)) return file.size;
const bytes = Number(file.size || file.fileSize || 0);
if (!bytes) return '-';
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
},
payload() {
return {
id: this.form.id,
billNo: this.form.billNo,
issuerId: this.form.issuerId,
receiverName: this.form.receiverName,
billType: this.form.billType,
faceAmount: this.form.faceAmount,
issueDate: this.form.issueDate,
maturityDate: this.form.maturityDate,
availableDeptIdsJson: JSON.stringify(this.form.availableDeptIds || []),
availableDeptNames: this.form.availableDeptNames,
feeBearerId: this.form.feeBearerId,
confirmedDiscountRate: this.form.confirmedDiscountRate,
issuingBank: this.form.issuingBank,
bankDiscountReferenceRate: this.form.bankDiscountReferenceRate,
attachmentsJson: JSON.stringify(this.form.attachments || []),
remark: this.form.remark,
};
},
async confirmSubmit() {
await this.$refs.formRef.validate();
this.saving = true;
try {
await api.submit(this.payload());
this.$message.success(this.recordId ? '编辑成功' : '新增成功');
this.goBack();
} finally {
this.saving = false;
}
},
goBack() {
this.$router.push('/payment/bill-ledger');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.bill-ledger-form-page__title,
.bill-ledger-form-page__section-title {
display: flex;
align-items: center;
margin-bottom: 16px;
font-size: 18px;
font-weight: 600;
}
.bill-ledger-form-page__title::before,
.bill-ledger-form-page__section-title::before {
width: 4px;
height: 20px;
margin-right: 8px;
background: #409eff;
content: '';
}
.bill-ledger-form-page__section {
margin-bottom: 24px;
}
.bill-ledger-form-page__section-title {
font-size: 16px;
}
.bill-ledger-form-page__form :deep(.el-form-item) {
margin-bottom: 16px;
}
.bill-ledger-form-page__form :deep(.el-input),
.bill-ledger-form-page__form :deep(.el-select),
.bill-ledger-form-page__form :deep(.el-input-number),
.bill-ledger-form-page__form :deep(.el-date-editor),
.bill-ledger-form-page__form :deep(.el-tree-select) {
width: 100%;
}
.bill-ledger-form-page__remark :deep(.el-form-item__content) {
margin-left: 0 !important;
}
.bill-ledger-form-page__uploader {
margin-top: 12px;
}
.bill-ledger-form-page__actions {
display: flex;
justify-content: center;
gap: 8px;
padding: 16px 0 8px;
}
.bill-ledger-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.bill-ledger-form-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
@media (max-width: 1024px) {
.bill-ledger-form-page :deep(.el-col-12) {
max-width: 100%;
flex: 0 0 100%;
}
}
</style>
+116
View File
@@ -0,0 +1,116 @@
<template>
<basic-container class="bill-ledger-page">
<bill-ledger-search
:query="query"
:counts="counts"
:loading="loading"
@search="handleSearch"
@reset="resetSearch"
/>
<bill-ledger-table
:rows="rows"
:loading="loading"
:page="page"
@add="openCreate"
@edit="openEdit"
@delete="handleDelete"
@page-change="handlePageChange"
@size-change="handleSizeChange"
/>
</basic-container>
</template>
<script>
import * as api from '@/api/payment/billLedger';
import BillLedgerSearch from './components/bill-ledger-search.vue';
import BillLedgerTable from './components/bill-ledger-table.vue';
const emptyQuery = () => ({
billNo: '',
issueDateRange: [],
issuerName: '',
receiverName: '',
billType: '',
maturityStatus: '',
expiryShortcut: 'all',
});
export default {
name: 'BillLedger',
components: { BillLedgerSearch, BillLedgerTable },
data() {
return {
query: emptyQuery(),
counts: { all: 0, within30: 0, within90: 0, over90: 0 },
rows: [],
loading: false,
page: { current: 1, size: 10, total: 0 },
};
},
mounted() {
this.loadData();
},
methods: {
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
requestParams() {
const { issueDateRange, ...params } = this.query;
return {
...params,
issueStartDate: issueDateRange?.[0] || '',
issueEndDate: issueDateRange?.[1] || '',
};
},
async loadData() {
this.loading = true;
try {
const [listResponse, countResponse] = await Promise.all([
api.getList(this.page.current, this.page.size, this.requestParams()),
api.getExpiryCounts(),
]);
const data = this.unwrapData(listResponse);
this.rows = data.records || [];
this.page.total = Number(data.total || 0);
this.counts = { ...this.counts, ...(this.unwrapData(countResponse) || {}) };
} finally {
this.loading = false;
}
},
handleSearch() {
this.page.current = 1;
this.loadData();
},
resetSearch() {
this.query = emptyQuery();
this.handleSearch();
},
openCreate() {
this.$router.push({ path: '/payment/bill-ledger/form', query: { mode: 'add' } });
},
openEdit(row) {
this.$router.push({
path: '/payment/bill-ledger/form',
query: { mode: 'edit', id: row.id },
});
},
async handleDelete(row) {
await this.$confirm('确认删除该汇票台账?', '提示', { type: 'warning' });
await api.remove(row.id);
this.$message.success('删除成功');
if (this.rows.length === 1 && this.page.current > 1) this.page.current -= 1;
this.loadData();
},
handlePageChange(current) {
this.page.current = current;
this.loadData();
},
handleSizeChange(size) {
this.page.current = 1;
this.page.size = size;
this.loadData();
},
},
};
</script>
+434
View File
@@ -0,0 +1,434 @@
<template>
<basic-container class="bill-payment-form-page">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="right"
label-width="auto"
class="bill-payment-form-page__form"
>
<section class="bill-payment-form-page__section">
<div class="bill-payment-form-page__section-title">基本信息</div>
<el-row :gutter="48">
<el-col :span="12">
<el-form-item label="票据号码" prop="billLedgerId">
<el-input
v-model="form.billNo"
readonly
:disabled="readonly"
placeholder="请选择汇票台账"
>
<template #append>
<el-button :icon="Search" :disabled="readonly" @click="openBillDialog" />
</template>
</el-input>
</el-form-item>
<el-form-item label="可用余额">
<el-input :model-value="formatMoney(form.availableBalance)" disabled />
</el-form-item>
<el-form-item label="付款日期" prop="paymentDate">
<el-date-picker
v-model="form.paymentDate"
type="date"
value-format="YYYY-MM-DD"
:disabled="readonly"
placeholder="请选择"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="票面金额">
<el-input :model-value="formatMoney(form.faceAmount)" disabled />
</el-form-item>
<el-form-item label="本次使用" prop="usedAmount">
<el-input-number
v-model="form.usedAmount"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark" class="bill-payment-form-page__remark">
<el-input
v-model="form.remark"
:disabled="readonly"
type="textarea"
:rows="3"
maxlength="200"
show-word-limit
placeholder="请输入"
/>
</el-form-item>
</el-col>
</el-row>
</section>
<section class="bill-payment-form-page__section">
<div class="bill-payment-form-page__section-title">附件信息</div>
<el-table :data="form.attachments" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="文件名" min-width="220">
<template #default="{ row }">
<el-link
v-if="row.url || row.link"
type="primary"
:href="row.url || row.link"
target="_blank"
>
{{ row.originalName || row.name || '附件' }}
</el-link>
<span v-else>{{ row.originalName || row.name || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="240">
<template #default="{ row }">
<el-input v-model="row.description" :disabled="readonly" maxlength="200" />
</template>
</el-table-column>
<el-table-column label="文件大小" width="120" align="center">
<template #default="{ row }">{{ fileSize(row) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" width="130" align="center" />
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" />
<el-table-column label="操作" width="100" align="center">
<template #default="{ $index }">
<el-link v-if="!readonly" type="danger" @click="form.attachments.splice($index, 1)"
>删除</el-link
>
</template>
</el-table-column>
</el-table>
<vehicle-attachment-upload
v-if="!readonly"
v-model="form.attachments"
class="bill-payment-form-page__uploader"
:multiple="true"
:limit="20"
:max-size="10"
:file-types="attachmentFileTypes"
:show-file-list="false"
button-text="上传"
@change="normalizeAttachments"
/>
</section>
<div class="bill-payment-form-page__actions">
<template v-if="!readonly">
<el-button type="primary" :loading="saving" :disabled="submitting" @click="saveDraft">
保存
</el-button>
<el-button type="primary" :loading="submitting" :disabled="saving" @click="submitForm">
提交
</el-button>
</template>
<el-button @click="goBack">取消</el-button>
</div>
</el-form>
<el-dialog v-model="billDialog.visible" title="选择汇票台账" width="86%" append-to-body>
<div class="bill-payment-form-page__dialog-search">
<el-input
v-model="billDialog.keyword"
clearable
placeholder="票据号码、出票单位或收票单位"
@keyup.enter="loadBillOptions"
/>
<el-button type="primary" @click="loadBillOptions">查询</el-button>
</div>
<el-table
v-loading="billDialog.loading"
:data="billDialog.rows"
border
highlight-current-row
@current-change="billDialog.current = $event"
@row-dblclick="selectBill"
>
<el-table-column prop="billNo" label="票据编号" min-width="170" />
<el-table-column prop="issuerName" label="出票单位" min-width="160" />
<el-table-column prop="receiverName" label="收票单位" min-width="160" />
<el-table-column label="票面金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.faceAmount) }}</template>
</el-table-column>
<el-table-column label="可用余额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.availableBalance) }}</template>
</el-table-column>
<el-table-column prop="maturityDate" label="到期日期" min-width="120" />
</el-table>
<template #footer>
<el-button @click="billDialog.visible = false">取消</el-button>
<el-button type="primary" @click="selectBill">确定</el-button>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { Search } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/billPayment';
import * as billLedgerApi from '@/api/payment/billLedger';
const emptyForm = () => ({
id: null,
billLedgerId: null,
billNo: '',
faceAmount: 0,
availableBalance: 0,
usedAmount: null,
deptId: null,
deptName: '',
paymentDate: '',
attachments: [],
remark: '',
});
export default {
name: 'BillPaymentForm',
data() {
return {
Search,
form: emptyForm(),
saving: false,
submitting: false,
billDialog: { visible: false, loading: false, keyword: '', rows: [], current: null },
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
],
rules: {
billLedgerId: [{ required: true, message: '请选择汇票台账', trigger: 'change' }],
usedAmount: [{ validator: this.validateUsedAmount, trigger: 'change' }],
paymentDate: [{ required: true, message: '请选择付款日期', trigger: 'change' }],
remark: [{ max: 200, message: '备注不能超过200个字符', trigger: 'blur' }],
},
};
},
computed: {
...mapGetters(['userInfo']),
recordId() {
return this.$route.query.id || '';
},
readonly() {
return this.$route.query.mode === 'view';
},
},
created() {
this.initialize();
},
methods: {
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
parse(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
try {
return JSON.parse(value) || [];
} catch {
return [];
}
},
async initialize() {
if (this.recordId) {
const data = this.unwrapData(await api.getDetail(this.recordId));
this.form = {
...emptyForm(),
...data,
attachments: this.parse(data.attachmentsJson),
};
return;
}
this.form.deptId = this.userInfo?.deptId || this.userInfo?.dept_id || null;
this.form.deptName = this.userInfo?.deptName || this.userInfo?.dept_name || '';
this.form.paymentDate = this.$dayjs().format('YYYY-MM-DD');
},
validateUsedAmount(rule, value, callback) {
const amount = Number(value);
const balance = Number(this.form.availableBalance || 0);
if (!Number.isFinite(amount) || amount <= 0) {
callback(new Error('本次使用必须大于0'));
return;
}
if (amount > balance) {
callback(new Error('本次使用不能超过汇票可用余额'));
return;
}
callback();
},
async openBillDialog() {
this.billDialog.visible = true;
this.billDialog.keyword = this.form.billNo || '';
await this.loadBillOptions();
},
async loadBillOptions() {
this.billDialog.loading = true;
try {
this.billDialog.rows =
this.unwrapData(
await billLedgerApi.getAvailableOptions(
this.billDialog.keyword,
this.form.deptId,
this.form.billLedgerId
)
) || [];
} finally {
this.billDialog.loading = false;
}
},
selectBill(row) {
const selected = row?.id ? row : this.billDialog.current;
if (!selected) {
this.$message.warning('请选择汇票台账');
return;
}
Object.assign(this.form, {
billLedgerId: selected.id,
billNo: selected.billNo,
faceAmount: selected.faceAmount,
availableBalance: selected.availableBalance,
});
this.billDialog.visible = false;
this.$refs.formRef?.validateField('billLedgerId').catch(() => {});
this.$refs.formRef?.validateField('usedAmount').catch(() => {});
},
normalizeAttachments(files) {
const userName = this.userInfo?.realName || this.userInfo?.userName || '';
const time = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.form.attachments = (files || []).map(file => ({
...file,
description: file.description || '',
uploadUserName: file.uploadUserName || userName,
uploadTime: file.uploadTime || time,
}));
},
fileSize(file) {
if (typeof file.size === 'string' && /[a-z]/i.test(file.size)) return file.size;
const bytes = Number(file.size || file.fileSize || 0);
if (!bytes) return '-';
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
},
payload() {
return {
id: this.form.id,
billLedgerId: this.form.billLedgerId,
usedAmount: this.form.usedAmount,
paymentDate: this.form.paymentDate,
attachmentsJson: JSON.stringify(this.form.attachments || []),
remark: this.form.remark,
};
},
async saveDraft() {
await this.$refs.formRef.validate();
this.saving = true;
try {
const id = this.unwrapData(await api.save(this.payload()));
this.form.id = id;
this.$message.success('保存成功');
return true;
} finally {
this.saving = false;
}
},
async submitForm() {
await this.$refs.formRef.validate();
this.submitting = true;
try {
const id = this.unwrapData(await api.save(this.payload()));
this.form.id = id;
await api.submit({ id: this.form.id });
this.$message.success('提交成功');
this.goBack();
} finally {
this.submitting = false;
}
},
goBack() {
this.$router.push('/payment/bill-payment');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.bill-payment-form-page__section {
margin-bottom: 24px;
}
.bill-payment-form-page__section-title {
display: flex;
align-items: center;
margin-bottom: 16px;
font-size: 18px;
font-weight: 600;
}
.bill-payment-form-page__section-title::before {
width: 4px;
height: 20px;
margin-right: 8px;
background: #409eff;
content: '';
}
.bill-payment-form-page__form :deep(.el-form-item) {
margin-bottom: 16px;
}
.bill-payment-form-page__form :deep(.el-input),
.bill-payment-form-page__form :deep(.el-input-number),
.bill-payment-form-page__form :deep(.el-date-editor) {
width: 100%;
}
.bill-payment-form-page__remark :deep(.el-form-item__content) {
margin-left: 0 !important;
}
.bill-payment-form-page__uploader {
margin-top: 12px;
}
.bill-payment-form-page__actions {
display: flex;
justify-content: flex-start;
gap: 8px;
padding: 16px 0 8px;
}
.bill-payment-form-page__dialog-search {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 12px;
}
.bill-payment-form-page__dialog-search .el-input {
width: 360px;
}
.bill-payment-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.bill-payment-form-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
@media (max-width: 1024px) {
.bill-payment-form-page :deep(.el-col-12) {
max-width: 100%;
flex: 0 0 100%;
}
}
</style>
+146
View File
@@ -0,0 +1,146 @@
<template>
<basic-container class="bill-payment-page">
<bill-payment-search
:query="query"
:loading="loading"
@search="handleSearch"
@reset="resetSearch"
/>
<bill-payment-table
:rows="rows"
:loading="loading"
:page="page"
@add="openCreate"
@view="openView"
@edit="openEdit"
@delete="handleDelete"
@flow="openFlow"
@void="handleVoid"
@page-change="handlePageChange"
@size-change="handleSizeChange"
/>
<el-dialog v-model="flowDialog.visible" title="审批流程" width="620px" append-to-body>
<el-descriptions :column="1" border>
<el-descriptions-item label="单据号">
{{ flowDialog.row.paymentNo || '-' }}
</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 * as api from '@/api/payment/billPayment';
import BillPaymentSearch from './components/bill-payment-search.vue';
import BillPaymentTable from './components/bill-payment-table.vue';
const emptyQuery = () => ({
paymentNo: '',
deptName: '',
paymentDateRange: [],
approvalStatus: '',
});
export default {
name: 'BillPayment',
components: { BillPaymentSearch, BillPaymentTable },
data() {
return {
query: emptyQuery(),
rows: [],
loading: false,
page: { current: 1, size: 10, total: 0 },
flowDialog: { visible: false, row: {} },
};
},
mounted() {
this.loadTable();
},
methods: {
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
buildParams() {
const params = { ...this.query };
const range = params.paymentDateRange || [];
delete params.paymentDateRange;
if (range.length === 2) {
params.paymentStartDate = range[0];
params.paymentEndDate = range[1];
}
return params;
},
async loadTable() {
this.loading = true;
try {
const data = this.unwrapData(
await api.getList(this.page.current, this.page.size, this.buildParams())
);
this.rows = data.records || [];
this.page.total = Number(data.total || 0);
} finally {
this.loading = false;
}
},
handleSearch() {
this.page.current = 1;
this.loadTable();
},
resetSearch() {
this.query = emptyQuery();
this.handleSearch();
},
openCreate() {
this.$router.push({ path: '/payment/bill-payment/form', query: { mode: 'add' } });
},
openView(row) {
this.$router.push({
path: '/payment/bill-payment/form',
query: { mode: 'view', id: row.id },
});
},
openEdit(row) {
this.$router.push({
path: '/payment/bill-payment/form',
query: { mode: 'edit', id: row.id },
});
},
async handleDelete(row) {
await this.$confirm('确认删除该汇票付款?', '提示', { type: 'warning' });
await api.remove(row.id);
this.$message.success('删除成功');
if (this.rows.length === 1 && this.page.current > 1) this.page.current -= 1;
this.loadTable();
},
openFlow(row) {
this.flowDialog.row = row;
this.flowDialog.visible = true;
},
async handleVoid(row) {
const { value } = await this.$prompt('请输入作废原因', '作废汇票付款');
await api.voidBill({ id: row.id, reason: value });
this.$message.success('作废成功');
this.loadTable();
},
handlePageChange(current) {
this.page.current = current;
this.loadTable();
},
handleSizeChange(size) {
this.page.current = 1;
this.page.size = size;
this.loadTable();
},
},
};
</script>
@@ -0,0 +1,149 @@
<template>
<section class="bill-ledger-search">
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
<div class="bill-ledger-search__grid">
<el-form-item label="票据编号">
<el-input v-model="query.billNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="出票日期">
<el-date-picker
v-model="query.issueDateRange"
type="daterange"
unlink-panels
clearable
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="出票单位">
<el-input v-model="query.issuerName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="收票单位">
<el-input v-model="query.receiverName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-if="expanded" label="汇票类型">
<el-select v-model="query.billType" clearable placeholder="请选择">
<el-option
v-for="item in billTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-if="expanded" label="到期状态">
<el-select v-model="query.maturityStatus" clearable placeholder="请选择">
<el-option
v-for="item in maturityStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</div>
<div class="bill-ledger-search__shortcut-row">
<span class="bill-ledger-search__shortcut-label">到期快捷筛选</span>
<el-segmented v-model="query.expiryShortcut" :options="shortcutOptions" />
</div>
<div class="bill-ledger-search__actions">
<el-button type="primary" :loading="loading" @click="$emit('search')">查询</el-button>
<el-button @click="$emit('reset')">重置</el-button>
<el-tooltip :content="expanded ? '折叠' : '展开'" placement="top">
<el-button text @click="expanded = !expanded">
<el-icon><component :is="expanded ? ArrowUp : ArrowDown" /></el-icon>
</el-button>
</el-tooltip>
</div>
</el-form>
</section>
</template>
<script>
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import { billTypeOptions, maturityStatusOptions } from '@/api/payment/billLedger';
export default {
name: 'BillLedgerSearch',
props: {
query: { type: Object, required: true },
counts: { type: Object, default: () => ({}) },
loading: { type: Boolean, default: false },
},
emits: ['search', 'reset'],
data() {
return {
ArrowDown,
ArrowUp,
expanded: false,
billTypeOptions,
maturityStatusOptions,
};
},
computed: {
shortcutOptions() {
return [
{ label: `全部(${this.counts.all || 0}`, value: 'all' },
{ label: `30天内到期(${this.counts.within30 || 0}`, value: 'within30' },
{ label: `90天内到期(${this.counts.within90 || 0}`, value: 'within90' },
{ label: `90天以上(${this.counts.over90 || 0}`, value: 'over90' },
];
},
},
};
</script>
<style scoped lang="scss">
.bill-ledger-search {
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.bill-ledger-search__grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px 24px;
}
.bill-ledger-search :deep(.el-form-item) {
margin-bottom: 8px;
}
.bill-ledger-search :deep(.el-input),
.bill-ledger-search :deep(.el-select),
.bill-ledger-search :deep(.el-date-editor) {
width: 100%;
}
.bill-ledger-search__shortcut-row {
display: flex;
align-items: center;
min-height: 40px;
}
.bill-ledger-search__shortcut-label {
width: 160px;
padding-right: 12px;
color: var(--el-text-color-regular);
text-align: right;
white-space: nowrap;
}
.bill-ledger-search__shortcut-row :deep(.el-segmented) {
--el-segmented-item-selected-bg-color: #409eff;
--el-segmented-item-selected-color: #fff;
}
.bill-ledger-search__actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 8px;
}
@media (max-width: 1200px) {
.bill-ledger-search__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.bill-ledger-search__shortcut-row {
align-items: flex-start;
}
.bill-ledger-search__shortcut-row :deep(.el-segmented) {
max-width: calc(100% - 160px);
}
}
</style>
@@ -0,0 +1,121 @@
<template>
<section class="bill-ledger-table">
<div class="bill-ledger-table__toolbar">
<div class="bill-ledger-table__title">汇票台账列表</div>
<el-button v-if="hasPermission('bill_ledger_add')" type="primary" @click="$emit('add')">
新增
</el-button>
</div>
<el-table v-loading="loading" :data="rows" border>
<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 }">
<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 label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<div class="bill-ledger-table__links">
<el-link
v-if="hasPermission('bill_ledger_edit')"
type="primary"
@click="$emit('edit', row)"
>
编辑
</el-link>
<el-link
v-if="hasPermission('bill_ledger_delete')"
type="danger"
@click="$emit('delete', row)"
>
删除
</el-link>
</div>
</template>
</el-table-column>
</el-table>
<div class="bill-ledger-table__pagination">
<el-pagination
:current-page="page.current"
:page-size="page.size"
:total="page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="$emit('page-change', $event)"
@size-change="$emit('size-change', $event)"
/>
</div>
</section>
</template>
<script>
import { mapGetters } from 'vuex';
import { billLedgerTableColumns } from '@/option/payment/billLedger';
export default {
name: 'BillLedgerTable',
props: {
rows: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
page: { type: Object, required: true },
},
emits: ['add', 'edit', 'delete', 'page-change', 'size-change'],
data() {
return { columns: billLedgerTableColumns };
},
computed: {
...mapGetters(['permission']),
},
methods: {
hasPermission(code) {
return this.permission?.[code] !== false;
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.bill-ledger-table {
margin-top: 8px;
}
.bill-ledger-table__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
}
.bill-ledger-table__title {
font-size: 16px;
font-weight: 600;
}
.bill-ledger-table__links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.bill-ledger-table__pagination {
display: flex;
justify-content: flex-end;
padding: 16px 0;
}
.bill-ledger-table :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.bill-ledger-table :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
</style>
@@ -0,0 +1,94 @@
<template>
<section class="bill-payment-search">
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
<div class="bill-payment-search__grid">
<el-form-item label="单据号">
<el-input v-model="query.paymentNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="使用部门">
<el-input v-model="query.deptName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="付款日期">
<el-date-picker
v-model="query.paymentDateRange"
type="daterange"
unlink-panels
clearable
value-format="YYYY-MM-DD"
range-separator="~"
start-placeholder="//"
end-placeholder="//"
/>
</el-form-item>
<el-form-item label="单据状态">
<el-select v-model="query.approvalStatus" clearable placeholder="请选择">
<el-option
v-for="item in approvalStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</div>
<div class="bill-payment-search__actions">
<el-button type="primary" :loading="loading" @click="$emit('search')">查询</el-button>
<el-button @click="$emit('reset')">重置</el-button>
<el-tooltip :content="expanded ? '折叠' : '展开'" placement="top">
<el-button text @click="expanded = !expanded">
<el-icon><component :is="expanded ? ArrowUp : ArrowDown" /></el-icon>
</el-button>
</el-tooltip>
</div>
</el-form>
</section>
</template>
<script>
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import { approvalStatusOptions } from '@/api/payment/billPayment';
export default {
name: 'BillPaymentSearch',
props: {
query: { type: Object, required: true },
loading: { type: Boolean, default: false },
},
emits: ['search', 'reset'],
data() {
return { ArrowDown, ArrowUp, expanded: false, approvalStatusOptions };
},
};
</script>
<style scoped lang="scss">
.bill-payment-search {
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.bill-payment-search__grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px 24px;
}
.bill-payment-search :deep(.el-form-item) {
margin-bottom: 8px;
}
.bill-payment-search :deep(.el-input),
.bill-payment-search :deep(.el-select),
.bill-payment-search :deep(.el-date-editor) {
width: 100%;
}
.bill-payment-search__actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 8px;
}
@media (max-width: 1200px) {
.bill-payment-search__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
@@ -0,0 +1,149 @@
<template>
<section class="bill-payment-table">
<div class="bill-payment-table__toolbar">
<el-button v-if="hasPermission('bill_payment_add')" type="primary" @click="$emit('add')">
新增
</el-button>
</div>
<el-table v-loading="loading" :data="rows" border>
<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-tag v-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="220" fixed="right" align="center">
<template #default="{ row }">
<div class="bill-payment-table__links">
<el-link
v-if="
['reviewing', 'approved', 'voided'].includes(row.approvalStatus) &&
hasPermission('bill_payment_view')
"
type="primary"
@click="$emit('view', row)"
>查看</el-link
>
<el-link
v-if="row.approvalStatus === 'draft' && hasPermission('bill_payment_delete')"
type="danger"
@click="$emit('delete', row)"
>删除</el-link
>
<el-link
v-if="
['draft', 'returned'].includes(row.approvalStatus) &&
hasPermission('bill_payment_edit')
"
type="primary"
@click="$emit('edit', row)"
>编辑</el-link
>
<el-link
v-if="row.approvalStatus !== 'draft'"
type="primary"
@click="$emit('flow', row)"
>流程</el-link
>
<el-link
v-if="row.approvalStatus === 'approved' && hasPermission('bill_payment_void')"
type="danger"
@click="$emit('void', row)"
>作废</el-link
>
</div>
</template>
</el-table-column>
</el-table>
<div class="bill-payment-table__pagination">
<el-pagination
:current-page="page.current"
:page-size="page.size"
:total="page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="$emit('page-change', $event)"
@size-change="$emit('size-change', $event)"
/>
</div>
</section>
</template>
<script>
import { mapGetters } from 'vuex';
import { billPaymentTableColumns } from '@/option/payment/billPayment';
export default {
name: 'BillPaymentTable',
props: {
rows: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
page: { type: Object, required: true },
},
emits: ['add', 'view', 'edit', 'delete', 'flow', 'void', 'page-change', 'size-change'],
data() {
return { columns: billPaymentTableColumns };
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
return String(this.userInfo?.authority || '').includes('admin');
},
},
methods: {
hasPermission(code) {
return this.isAdmin || this.permission?.[code] === true;
},
statusType(status) {
return (
{ reviewing: 'warning', returned: 'danger', approved: 'success', voided: 'info' }[status] ||
''
);
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.bill-payment-table {
margin-top: 8px;
}
.bill-payment-table__toolbar {
display: flex;
justify-content: flex-start;
padding: 12px 0;
}
.bill-payment-table__links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.bill-payment-table__pagination {
display: flex;
justify-content: flex-end;
padding: 16px 0;
}
.bill-payment-table :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.bill-payment-table :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
</style>
File diff suppressed because it is too large Load Diff
+474
View File
@@ -0,0 +1,474 @@
<template>
<basic-container class="invoice-page">
<section class="invoice-page__search">
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
<div class="invoice-page__search-grid">
<el-form-item label="单据号">
<el-input v-model="query.applicationNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="所属项目">
<el-input v-model="query.projectName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="所属组织">
<el-input v-model="query.deptName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="金蝶单据状态">
<el-select v-model="query.kingdeeStatus" clearable placeholder="请选择">
<el-option
v-for="item in kingdeeStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-if="searchExpanded" label="状态">
<el-select v-model="query.approvalStatus" clearable placeholder="请选择">
<el-option
v-for="item in approvalStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</div>
<div class="invoice-page__search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-tooltip :content="searchExpanded ? '折叠' : '展开'" placement="top">
<el-button text @click="searchExpanded = !searchExpanded">
<el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon>
</el-button>
</el-tooltip>
</div>
</el-form>
</section>
<section class="invoice-page__table-panel">
<div class="invoice-page__toolbar">
<div class="invoice-page__table-title">开票申请单列表</div>
<div class="invoice-page__toolbar-actions">
<el-button
v-if="hasPermission('invoice_application_sync')"
type="primary"
plain
@click="handleBatchSync"
>批量同步</el-button
>
<el-button
v-if="hasPermission('invoice_application_add')"
type="primary"
@click="openCreate"
>新增</el-button
>
<el-button
v-if="hasPermission('invoice_application_export')"
type="primary"
plain
@click="handleExport"
>导出</el-button
>
<el-tooltip content="刷新" placement="top">
<el-button text :icon="Refresh" @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]) }}</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="invoice-page__links">
<el-link
v-if="hasPermission('invoice_application_view')"
type="primary"
@click="openView(row)"
>查看</el-link
>
<el-link
v-if="
hasPermission('invoice_application_edit') &&
['draft', 'returned'].includes(row.approvalStatus)
"
type="primary"
@click="openEdit(row)"
>编辑</el-link
>
<el-link
v-if="hasPermission('invoice_application_delete') && row.approvalStatus === 'draft'"
type="danger"
@click="handleDelete(row)"
>删除</el-link
>
<el-link
v-if="
hasPermission('invoice_application_submit') &&
['draft', 'returned'].includes(row.approvalStatus)
"
type="primary"
@click="handleSubmit(row)"
>提交</el-link
>
<el-link
v-if="hasPermission('invoice_application_view') && row.approvalStatus !== 'draft'"
type="primary"
@click="openFlow(row)"
>流程</el-link
>
<el-link
v-if="
hasPermission('invoice_application_approve') && row.approvalStatus === 'reviewing'
"
type="primary"
@click="handleApprove(row)"
>通过</el-link
>
<el-link
v-if="
hasPermission('invoice_application_approve') && row.approvalStatus === 'reviewing'
"
type="danger"
@click="handleReturn(row)"
>驳回</el-link
>
<el-link
v-if="
hasPermission('invoice_application_void') && row.approvalStatus === 'approved'
"
type="danger"
@click="handleVoid(row)"
>作废</el-link
>
</div>
</template>
</el-table-column>
</el-table>
<div class="invoice-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>
<el-dialog v-model="flowDialog.visible" title="审批流程" width="900px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="开票申请单号">
{{ flowDialog.row.applicationNo || '-' }}
</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-table
v-loading="flowDialog.loading"
:data="flowDialog.records"
border
class="invoice-page__flow-table"
>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="actionName" label="操作" min-width="110" align="center" />
<el-table-column label="原状态" min-width="100" align="center">
<template #default="{ row }">{{ statusName(row.fromStatus) }}</template>
</el-table-column>
<el-table-column label="新状态" min-width="100" align="center">
<template #default="{ row }">{{ statusName(row.toStatus) }}</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作人" min-width="110" align="center" />
<el-table-column prop="createTime" label="操作时间" min-width="170" align="center" />
<el-table-column prop="reason" label="原因" min-width="160" show-overflow-tooltip />
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" />
</el-table>
</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/payment/invoiceApplication';
import { invoiceApplicationTableColumns } from '@/option/payment/invoiceApplication';
const emptyQuery = () => ({
applicationNo: '',
projectName: '',
deptName: '',
kingdeeStatus: '',
approvalStatus: '',
});
export default {
name: 'InvoiceApplication',
data() {
return {
ArrowDown,
ArrowUp,
Refresh,
query: emptyQuery(),
searchExpanded: false,
approvalStatusOptions: api.approvalStatusOptions,
kingdeeStatusOptions: api.kingdeeStatusOptions,
columns: invoiceApplicationTableColumns,
rows: [],
selection: [],
loading: false,
page: { current: 1, size: 10, total: 0 },
flowDialog: { visible: false, loading: false, row: {}, records: [] },
};
},
computed: {
...mapGetters(['permission']),
},
mounted() {
this.loadTable();
},
methods: {
hasPermission(code) {
return this.permission?.[code] !== false;
},
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async loadTable() {
this.loading = true;
try {
const data = this.unwrapData(
await api.getList(this.page.current, this.page.size, this.query)
);
this.rows = data.records || [];
this.page.total = Number(data.total || 0);
} finally {
this.loading = false;
}
},
handleSearch() {
this.page.current = 1;
this.loadTable();
},
resetSearch() {
this.query = emptyQuery();
this.handleSearch();
},
handleSizeChange() {
this.page.current = 1;
this.loadTable();
},
openCreate() {
this.$router.push({ path: '/payment/invoice-application/form', query: { mode: 'add' } });
},
openEdit(row) {
this.$router.push({
path: '/payment/invoice-application/form',
query: { mode: 'edit', id: row.id },
});
},
openView(row) {
this.$router.push({
path: '/payment/invoice-application/form',
query: { mode: 'view', id: row.id },
});
},
async openFlow(row) {
this.flowDialog.row = { ...row };
this.flowDialog.records = [];
this.flowDialog.visible = true;
this.flowDialog.loading = true;
try {
const detail = this.unwrapData(await api.getDetail(row.id));
this.flowDialog.row = { ...row, ...detail };
this.flowDialog.records = detail.records || [];
} finally {
this.flowDialog.loading = false;
}
},
async handleDelete(row) {
await this.$confirm('确认删除该开票申请?', '提示', { type: 'warning' });
await api.remove(row.id);
this.$message.success('删除成功');
this.loadTable();
},
async handleSubmit(row) {
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('请输入驳回原因', '审批驳回');
await api.returnBill({ id: row.id, reason: value });
this.$message.success('已驳回');
this.loadTable();
},
async handleVoid(row) {
const { value } = await this.$prompt('请输入作废原因', '作废开票申请');
await api.voidBill({ id: row.id, reason: value });
this.$message.success('作废成功');
this.loadTable();
},
async handleBatchSync() {
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
if (!rows.length) {
this.$message.warning('请选择至少一条审批通过的开票申请');
return;
}
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
this.$message.success(`已同步${rows.length}条开票申请`);
this.loadTable();
},
async handleExport() {
const data = this.unwrapData(await api.getList(1, 100000, this.query));
const exportRows = (data.records || []).map(item => ({
单据号: item.applicationNo,
结算单号: item.settlementNos,
所属项目: item.projectName,
所属组织: item.deptName,
开票方: item.issuerName,
受票方: item.receiverName,
开票金额: Number(item.invoiceAmount || 0).toFixed(2),
创建人: item.createUserName,
创建时间: item.createTime,
状态: item.approvalStatusName,
当前节点: item.currentNode,
当前处理人: item.currentProcessor,
金蝶单据号: item.kingdeeBillNo,
金蝶单据状态: item.kingdeeStatusName,
}));
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`);
},
statusType(status) {
return (
{ approved: 'success', reviewing: 'warning', returned: 'danger', voided: 'info' }[status] ||
''
);
},
statusName(status) {
return (
{
draft: '草稿',
reviewing: '审批中',
approved: '审批通过',
returned: '已驳回',
voided: '已作废',
}[status] ||
status ||
'-'
);
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.invoice-page__search {
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.invoice-page__search-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px 24px;
}
.invoice-page__search :deep(.el-form-item) {
margin-bottom: 8px;
}
.invoice-page__search :deep(.el-input),
.invoice-page__search :deep(.el-select) {
width: 100%;
}
.invoice-page__search-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 8px;
}
.invoice-page__table-panel {
margin-top: 8px;
}
.invoice-page__toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
}
.invoice-page__toolbar-actions,
.invoice-page__links {
display: flex;
align-items: center;
gap: 8px;
}
.invoice-page__table-title {
font-size: 16px;
font-weight: 600;
}
.invoice-page__links {
flex-wrap: wrap;
justify-content: center;
}
.invoice-page__pagination {
display: flex;
justify-content: flex-end;
padding: 16px 0;
}
.invoice-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.invoice-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
.invoice-page__flow-table {
margin-top: 16px;
}
@media (max-width: 1200px) {
.invoice-page__search-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
+834
View File
@@ -0,0 +1,834 @@
<template>
<basic-container class="receipt-form-page">
<div class="receipt-form-page__title">
{{ readonly ? '查看收票登记' : recordId ? '编辑收票登记' : '新增收票登记' }}
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="right"
label-width="auto"
class="receipt-form-page__form"
>
<section class="receipt-form-page__section">
<div class="receipt-form-page__section-title">收票基本信息</div>
<el-row :gutter="32">
<el-col :span="12">
<el-form-item label="发票号码" prop="invoiceNo">
<el-input v-model="form.invoiceNo" readonly placeholder="请选择金蝶票据池发票">
<template v-if="!readonly" #append>
<el-tooltip content="查询金蝶票据池" placement="top">
<el-button :icon="Search" @click="openInvoiceDialog" />
</el-tooltip>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开票日期">
<el-input v-model="form.invoiceDate" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发票类型">
<el-input v-model="form.invoiceType" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="税率">
<el-input
:model-value="formatRate(form.taxRate)"
disabled
placeholder="从金蝶票据池带出"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开票金额">
<el-input
:model-value="form.kingdeeInvoicePoolId ? formatMoney(form.invoiceAmount) : ''"
disabled
placeholder="从金蝶票据池带出"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="税额">
<el-input
:model-value="form.kingdeeInvoicePoolId ? formatMoney(form.taxAmount) : ''"
disabled
placeholder="从金蝶票据池带出"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="受票单位">
<el-input v-model="form.receiverName" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开票单位">
<el-input v-model="form.issuerName" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="关联结算单" prop="settlementIds">
<el-input :model-value="settlementLabel" readonly placeholder="请选择应付正式结算单">
<template v-if="!readonly" #append>
<el-tooltip content="选择正式结算单" placement="top">
<el-button :icon="Search" @click="openSettlementDialog" />
</el-tooltip>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开户行">
<el-input v-model="form.bankName" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开户账号">
<el-input v-model="form.bankAccount" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开票行">
<el-input v-model="form.issuingBank" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="电话">
<el-input v-model="form.phone" :disabled="readonly" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户邮箱" prop="customerEmails">
<el-select
v-model="form.customerEmails"
:disabled="readonly"
multiple
filterable
allow-create
default-first-option
:multiple-limit="3"
>
<el-option
v-for="email in customerEmailOptions"
:key="email"
:label="email"
:value="email"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="部门邮箱" prop="departmentEmails">
<el-select
v-model="form.departmentEmails"
:disabled="readonly"
multiple
filterable
allow-create
default-first-option
:multiple-limit="3"
>
<el-option
v-for="email in departmentEmailOptions"
:key="email"
:label="email"
:value="email"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</section>
<section class="receipt-form-page__section">
<div class="receipt-form-page__section-title">结算信息</div>
<el-table :data="form.settlements" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="结算单号" min-width="180" align="center">
<template #default="{ row }">
<el-link type="primary" @click="openSettlementDetail(row)">
{{ row.formalSettlementNo }}
</el-link>
</template>
</el-table-column>
<el-table-column label="结算总金额" min-width="150" align="right">
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
</el-table-column>
<el-table-column label="已收票金额" min-width="150" align="right">
<template #default="{ row }">{{ formatMoney(row.receivedInvoiceAmount) }}</template>
</el-table-column>
<el-table-column label="分摊发票金额" min-width="180">
<template #default="{ row }">
<el-input-number
v-model="row.allocatedInvoiceAmount"
:disabled="readonly"
:min="0"
:max="remainingAmount(row)"
:precision="2"
:controls="false"
/>
</template>
</el-table-column>
</el-table>
</section>
<section class="receipt-form-page__section">
<div class="receipt-form-page__section-heading">
<div class="receipt-form-page__section-title">附件信息</div>
<el-button
v-if="form.attachments.length"
type="primary"
plain
:icon="Download"
@click="downloadAttachments"
>批量下载</el-button
>
</div>
<el-table :data="form.attachments" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="文件名" min-width="220">
<template #default="{ row }">{{ row.originalName || row.name || '-' }}</template>
</el-table-column>
<el-table-column label="附件描述" min-width="240">
<template #default="{ row }">
<el-input v-model="row.description" :disabled="readonly" maxlength="200" />
</template>
</el-table-column>
<el-table-column prop="size" label="文件大小" width="120" />
<el-table-column prop="uploadUserName" label="上传人" width="140" />
<el-table-column prop="uploadTime" label="上传时间" width="170" />
<el-table-column label="操作" width="120" align="center">
<template #default="{ row, $index }">
<div class="receipt-form-page__links">
<el-link type="primary" :href="row.url || row.link" target="_blank">查看</el-link>
<el-link v-if="!readonly" type="danger" @click="form.attachments.splice($index, 1)"
>删除</el-link
>
</div>
</template>
</el-table-column>
</el-table>
<vehicle-attachment-upload
v-model="form.attachments"
:readonly="readonly"
:multiple="true"
:limit="20"
:max-size="500"
:file-types="attachmentFileTypes"
:show-file-list="false"
button-text="上传附件"
class="receipt-form-page__uploader"
@change="normalizeAttachments"
/>
</section>
<section class="receipt-form-page__section">
<div class="receipt-form-page__section-title">备注</div>
<el-form-item class="receipt-form-page__remark">
<el-input
v-model="form.remark"
:disabled="readonly"
type="textarea"
:rows="3"
maxlength="200"
show-word-limit
/>
</el-form-item>
</section>
<div class="receipt-form-page__actions">
<el-button v-if="!readonly" type="primary" plain @click="syncReferenceData">同步</el-button>
<el-button v-if="!readonly && canSave" @click="saveDraft">保存</el-button>
<el-button
v-if="!readonly && canSave && hasPermission('invoice_receipt_submit')"
type="primary"
@click="submitForm"
>提交</el-button
>
<el-button @click="goBack">返回</el-button>
</div>
</el-form>
<el-dialog v-model="invoiceDialog.visible" title="查询金蝶票据池" width="86%" append-to-body>
<div class="receipt-form-page__dialog-search">
<el-input
v-model="invoiceDialog.keyword"
clearable
placeholder="发票号码、开票单位或受票单位"
@keyup.enter="loadInvoicePool"
/>
<el-button type="primary" @click="loadInvoicePool">查询</el-button>
</div>
<el-table
v-loading="invoiceDialog.loading"
:data="invoiceDialog.rows"
border
highlight-current-row
@current-change="invoiceDialog.current = $event"
@row-dblclick="confirmInvoice"
>
<el-table-column prop="invoiceNo" label="发票号码" min-width="180" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" />
<el-table-column prop="invoiceType" label="发票类型" min-width="130" />
<el-table-column prop="issuerName" label="开票单位" min-width="170" />
<el-table-column prop="receiverName" label="受票单位" min-width="170" />
<el-table-column label="开票金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column label="税率" min-width="100" align="center">
<template #default="{ row }">{{ formatRate(row.taxRate) }}</template>
</el-table-column>
<el-table-column label="税额" min-width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
</el-table-column>
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" />
</el-table>
<template #footer>
<el-button @click="invoiceDialog.visible = false">取消</el-button>
<el-button type="primary" @click="confirmInvoice()">确定</el-button>
</template>
</el-dialog>
<el-dialog
v-model="settlementDialog.visible"
title="选择应付正式结算单"
width="86%"
append-to-body
>
<div class="receipt-form-page__dialog-search">
<el-input
v-model="settlementDialog.keyword"
clearable
placeholder="结算单号、项目或合同"
@keyup.enter="loadSettlementCandidates"
/>
<el-button type="primary" @click="loadSettlementCandidates">查询</el-button>
</div>
<el-table
ref="settlementTable"
v-loading="settlementDialog.loading"
:data="settlementDialog.rows"
border
@selection-change="settlementDialog.selection = $event"
>
<el-table-column type="selection" width="52" align="center" />
<el-table-column prop="formalSettlementNo" label="结算单号" min-width="170" />
<el-table-column prop="projectName" label="所属项目" min-width="150" />
<el-table-column prop="deptName" label="所属组织" min-width="150" />
<el-table-column prop="contractNo" label="合同编号" min-width="150" />
<el-table-column prop="payerName" label="付款方" min-width="150" />
<el-table-column prop="payeeName" label="收款方" min-width="150" />
<el-table-column label="结算总金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
</el-table-column>
<el-table-column label="已收票金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.receivedInvoiceAmount) }}</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="settlementDialog.visible = false">取消</el-button>
<el-button type="primary" @click="confirmSettlements">确定</el-button>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { Download, Search } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/invoiceReceipt';
const emptyForm = () => ({
id: null,
kingdeeInvoicePoolId: null,
invoiceNo: '',
invoiceDate: '',
invoiceType: '',
taxRate: null,
invoiceAmount: null,
taxAmount: null,
receiverName: '',
issuerName: '',
bankName: '',
bankAccount: '',
issuingBank: '',
phone: '',
customerEmails: [],
departmentEmails: [],
projectName: '',
deptName: '',
payerName: '',
payeeName: '',
settlements: [],
attachments: [],
remark: '',
});
export default {
name: 'InvoiceReceiptForm',
data() {
return {
Download,
Search,
form: emptyForm(),
customerEmailOptions: [],
departmentEmailOptions: [],
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
],
invoiceDialog: { visible: false, loading: false, keyword: '', rows: [], current: null },
settlementDialog: {
visible: false,
loading: false,
keyword: '',
rows: [],
selection: [],
},
rules: {
invoiceNo: [{ required: true, validator: this.validateInvoice, trigger: 'change' }],
settlementIds: [{ required: true, validator: this.validateSettlements, trigger: 'change' }],
customerEmails: [{ validator: this.validateEmails, trigger: 'change' }],
departmentEmails: [{ validator: this.validateEmails, trigger: 'change' }],
},
};
},
computed: {
...mapGetters(['permission']),
recordId() {
return this.$route.query.id || '';
},
readonly() {
return this.$route.query.mode === 'view';
},
canSave() {
const code = this.recordId ? 'invoice_receipt_edit' : 'invoice_receipt_add';
return this.hasPermission(code);
},
settlementLabel() {
return this.form.settlements
.map(item => item.formalSettlementNo)
.filter(Boolean)
.join(',');
},
settlementIds() {
return this.form.settlements.map(item => item.formalSettlementId || item.settlementId);
},
allocatedTotal() {
return this.form.settlements.reduce(
(total, item) => total + Number(item.allocatedInvoiceAmount || 0),
0
);
},
},
created() {
this.initialize();
},
methods: {
hasPermission(code) {
return this.permission?.[code] !== false;
},
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
parse(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
try {
return JSON.parse(value) || [];
} catch {
return [];
}
},
parseEmails(value) {
return String(value || '')
.split(/[;,,;]/)
.map(item => item.trim())
.filter(Boolean);
},
async initialize() {
const userInfo = this.$store.getters.userInfo || {};
const userEmail = userInfo.email || '';
this.departmentEmailOptions = userEmail ? [userEmail] : [];
if (!this.recordId) {
if (userEmail) this.form.departmentEmails = [userEmail];
return;
}
const data = this.unwrapData(await api.getDetail(this.recordId));
this.form = {
...emptyForm(),
...data,
customerEmails: this.parseEmails(data.customerEmails),
departmentEmails: this.parseEmails(data.departmentEmails),
settlements: (data.settlements || []).map(item => ({
...item,
settlementId: item.formalSettlementId,
})),
attachments: this.parse(data.attachmentsJson),
};
if (this.settlementIds.length) await this.loadReferenceInformation();
},
validateInvoice(rule, value, callback) {
if (!value || !this.form.kingdeeInvoicePoolId) {
callback(new Error('请选择金蝶票据池发票'));
return;
}
callback();
},
validateSettlements(rule, value, callback) {
if (!this.form.settlements.length) {
callback(new Error('请选择应付正式结算单'));
return;
}
callback();
},
validateEmails(rule, value, callback) {
const emails = Array.isArray(value) ? value : this.parseEmails(value);
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emails.length > 3) {
callback(new Error('最多填写3个邮箱'));
return;
}
if (emails.some(email => !valid.test(email))) {
callback(new Error('请输入正确的邮箱'));
return;
}
callback();
},
async openInvoiceDialog() {
this.invoiceDialog.visible = true;
this.invoiceDialog.keyword = this.form.invoiceNo || '';
await this.loadInvoicePool();
},
async loadInvoicePool() {
this.invoiceDialog.loading = true;
try {
this.invoiceDialog.rows =
this.unwrapData(await api.getInvoicePool(this.invoiceDialog.keyword)) || [];
} finally {
this.invoiceDialog.loading = false;
}
},
confirmInvoice(row, options = {}) {
const selected = row?.id ? row : this.invoiceDialog.current;
if (!selected) {
this.$message.warning('请选择一张金蝶票据池发票');
return;
}
if (this.form.settlements.length) {
const first = this.form.settlements[0];
if (selected.receiverName !== first.payerName || selected.issuerName !== first.payeeName) {
this.$message.warning('金蝶发票的开票单位、受票单位与结算单收付款方不一致');
return;
}
}
const preserveUserInput = options.preserveUserInput === true;
Object.assign(this.form, {
kingdeeInvoicePoolId: selected.id,
invoiceNo: selected.invoiceNo,
invoiceDate: selected.invoiceDate,
invoiceType: selected.invoiceType,
taxRate: selected.taxRate,
invoiceAmount: selected.invoiceAmount,
taxAmount: selected.taxAmount,
receiverName: selected.receiverName,
issuerName: selected.issuerName,
bankName: selected.bankName,
bankAccount: selected.bankAccount,
issuingBank: selected.issuingBank,
phone: preserveUserInput ? this.form.phone : selected.phone || '',
customerEmails: preserveUserInput
? this.form.customerEmails
: this.parseEmails(selected.customerEmails),
departmentEmails: preserveUserInput
? this.form.departmentEmails
: this.parseEmails(selected.departmentEmails),
attachments: preserveUserInput
? this.form.attachments
: this.parse(selected.attachmentsJson),
});
this.customerEmailOptions = [...this.form.customerEmails];
this.departmentEmailOptions = [
...new Set([...this.departmentEmailOptions, ...this.form.departmentEmails]),
];
this.invoiceDialog.visible = false;
this.$refs.formRef?.validateField('invoiceNo').catch(() => {});
return true;
},
async openSettlementDialog() {
this.settlementDialog.visible = true;
await this.loadSettlementCandidates();
},
async loadSettlementCandidates() {
this.settlementDialog.loading = true;
try {
this.settlementDialog.rows =
this.unwrapData(
await api.getSettlementCandidates(this.settlementDialog.keyword, this.form.id)
) || [];
} finally {
this.settlementDialog.loading = false;
}
},
async confirmSettlements() {
const rows = this.settlementDialog.selection;
if (!rows.length) {
this.$message.warning('请至少选择一张应付正式结算单');
return;
}
const first = rows[0];
const incompatible = rows.some(
item =>
String(item.projectId) !== String(first.projectId) ||
String(item.deptId) !== String(first.deptId) ||
item.payerName !== first.payerName ||
item.payeeName !== first.payeeName
);
if (incompatible) {
this.$message.warning('关联结算单必须属于同一项目、组织及收付款方');
return;
}
if (
this.form.kingdeeInvoicePoolId &&
(this.form.receiverName !== first.payerName || this.form.issuerName !== first.payeeName)
) {
this.$message.warning('金蝶发票的开票单位、受票单位与结算单收付款方不一致');
return;
}
this.form.settlements = rows.map(item => ({
...item,
formalSettlementId: item.id,
settlementId: item.id,
allocatedInvoiceAmount: 0,
}));
this.form.projectName = first.projectName;
this.form.deptName = first.deptName;
this.form.payerName = first.payerName;
this.form.payeeName = first.payeeName;
this.settlementDialog.visible = false;
await this.loadReferenceInformation();
this.$refs.formRef?.validateField('settlementIds').catch(() => {});
},
async loadReferenceInformation() {
const data = this.unwrapData(await api.getReferenceInformation(this.settlementIds.join(',')));
this.customerEmailOptions = data.customerEmails || [];
this.departmentEmailOptions = [
...new Set([...this.departmentEmailOptions, ...(data.departmentEmails || [])]),
];
if (!this.form.customerEmails.length && this.customerEmailOptions.length) {
this.form.customerEmails = [this.customerEmailOptions[0]];
}
},
remainingAmount(row) {
return Math.max(
Number(row.settlementAmount || 0) - Number(row.receivedInvoiceAmount || 0),
0
);
},
normalizeAttachments(files) {
const userInfo = this.$store.getters.userInfo || {};
const userName = userInfo.realName || userInfo.userName || '';
const time = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.form.attachments = (files || []).map(file => ({
...file,
description: file.description || '',
uploadUserName: file.uploadUserName || userName,
uploadTime: file.uploadTime || time,
}));
},
downloadAttachments() {
this.form.attachments.forEach(file => {
const url = file.url || file.link;
if (!url) return;
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = file.originalName || file.name || '';
anchor.target = '_blank';
anchor.click();
});
},
openSettlementDetail(row) {
const id = row.formalSettlementId || row.settlementId || row.id;
if (!id) return;
this.$router.push({
path: '/settlement/formal-settlement/form',
query: { mode: 'view', id, name: '查看正式结算' },
});
},
validateBusiness() {
const invoiceAmount = Number(this.form.invoiceAmount || 0);
if (invoiceAmount <= 0) throw new Error('开票金额必须大于0');
if (Math.abs(this.allocatedTotal - invoiceAmount) > 0.001) {
throw new Error('分摊发票金额总和必须等于开票金额');
}
for (const row of this.form.settlements) {
const allocated = Number(row.allocatedInvoiceAmount);
if (!Number.isFinite(allocated) || allocated < 0) {
throw new Error('分摊发票金额必须大于或等于0');
}
if (allocated - this.remainingAmount(row) > 0.001) {
throw new Error(`结算单${row.formalSettlementNo}的累计收票金额不能超过结算总金额`);
}
}
},
payload() {
return {
id: this.form.id,
kingdeeInvoicePoolId: this.form.kingdeeInvoicePoolId,
phone: this.form.phone,
customerEmails: this.form.customerEmails.join(';'),
departmentEmails: this.form.departmentEmails.join(';'),
attachmentsJson: JSON.stringify(this.form.attachments || []),
remark: this.form.remark,
settlements: this.form.settlements.map(item => ({
settlementId: item.formalSettlementId || item.settlementId,
allocatedInvoiceAmount: item.allocatedInvoiceAmount,
})),
};
},
async saveDraft() {
await this.$refs.formRef.validate();
try {
this.validateBusiness();
} catch (error) {
this.$message.warning(error.message);
return false;
}
const id = this.unwrapData(await api.save(this.payload()));
this.form.id = id;
this.$message.success('保存成功');
return true;
},
async submitForm() {
const saved = await this.saveDraft();
if (!saved) return;
await api.submit({ id: this.form.id });
this.$message.success('提交成功');
this.goBack();
},
async syncReferenceData() {
if (!this.form.invoiceNo) {
this.$message.warning('请先选择金蝶票据池发票');
return;
}
const rows = this.unwrapData(await api.getInvoicePool(this.form.invoiceNo)) || [];
const invoice = rows.find(item => item.invoiceNo === this.form.invoiceNo);
if (!invoice) {
this.$message.warning('金蝶票据池中未查询到该发票');
return;
}
if (!this.confirmInvoice(invoice, { preserveUserInput: true })) return;
if (this.settlementIds.length) await this.loadReferenceInformation();
this.$message.success('同步成功');
},
goBack() {
this.$router.push('/payment/invoice-receipt');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
formatRate(value) {
return value === null || value === undefined || value === '' ? '' : `${Number(value)}%`;
},
},
};
</script>
<style scoped lang="scss">
.receipt-form-page__title,
.receipt-form-page__section-title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.receipt-form-page__title::before,
.receipt-form-page__section-title::before {
width: 4px;
height: 20px;
margin-right: 8px;
background: #409eff;
content: '';
}
.receipt-form-page__section {
margin-bottom: 24px;
}
.receipt-form-page__section-title {
font-size: 16px;
}
.receipt-form-page__section-heading {
display: flex;
align-items: center;
justify-content: space-between;
}
.receipt-form-page__section-heading .receipt-form-page__section-title {
margin-bottom: 0;
}
.receipt-form-page__form :deep(.el-form-item) {
margin-bottom: 16px;
}
.receipt-form-page__form :deep(.el-select),
.receipt-form-page__form :deep(.el-input-number) {
width: 100%;
}
.receipt-form-page__links {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.receipt-form-page__remark :deep(.el-form-item__content) {
margin-left: 0 !important;
}
.receipt-form-page__uploader {
margin-top: 12px;
}
.receipt-form-page__actions {
display: flex;
justify-content: center;
gap: 8px;
padding: 16px 0 8px;
}
.receipt-form-page__dialog-search {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 12px;
}
.receipt-form-page__dialog-search .el-input {
width: 360px;
}
.receipt-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.receipt-form-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
@media (max-width: 900px) {
.receipt-form-page__form :deep(.el-col-12) {
max-width: 100%;
flex: 0 0 100%;
}
}
</style>
+480
View File
@@ -0,0 +1,480 @@
<template>
<basic-container class="receipt-page">
<section class="receipt-page__search">
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
<div class="receipt-page__search-grid">
<el-form-item label="发票号码">
<el-input v-model="query.invoiceNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="开票日期">
<el-date-picker
v-model="query.invoiceDate"
type="date"
clearable
value-format="YYYY-MM-DD"
placeholder="请选择"
/>
</el-form-item>
<el-form-item label="所属项目">
<el-input v-model="query.projectName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="所属组织">
<el-input v-model="query.deptName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-if="searchExpanded" label="审核状态">
<el-select v-model="query.approvalStatus" clearable placeholder="请选择">
<el-option
v-for="item in approvalStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-if="searchExpanded" label="金蝶单据状态">
<el-select v-model="query.kingdeeStatus" clearable placeholder="请选择">
<el-option
v-for="item in kingdeeStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</div>
<div class="receipt-page__search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-tooltip :content="searchExpanded ? '折叠' : '展开'" placement="top">
<el-button text @click="searchExpanded = !searchExpanded">
<el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon>
</el-button>
</el-tooltip>
</div>
</el-form>
</section>
<section class="receipt-page__table-panel">
<div class="receipt-page__toolbar">
<div class="receipt-page__table-title">收票单列表</div>
<div class="receipt-page__toolbar-actions">
<el-button
v-if="hasPermission('invoice_receipt_sync')"
type="primary"
plain
@click="handleBatchSync"
>批量同步</el-button
>
<el-button v-if="hasPermission('invoice_receipt_add')" type="primary" @click="openCreate"
>新增</el-button
>
<el-button
v-if="hasPermission('invoice_receipt_export')"
type="primary"
plain
@click="handleExport"
>导出</el-button
>
<el-tooltip content="刷新" placement="top">
<el-button text :icon="Refresh" @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]) }}</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="receipt-page__links">
<el-link
v-if="hasPermission('invoice_receipt_view')"
type="primary"
@click="openView(row)"
>查看</el-link
>
<el-link
v-if="
hasPermission('invoice_receipt_edit') &&
['draft', 'returned'].includes(row.approvalStatus)
"
type="primary"
@click="openEdit(row)"
>编辑</el-link
>
<el-link
v-if="hasPermission('invoice_receipt_delete') && row.approvalStatus === 'draft'"
type="danger"
@click="handleDelete(row)"
>删除</el-link
>
<el-link
v-if="
hasPermission('invoice_receipt_submit') &&
['draft', 'returned'].includes(row.approvalStatus)
"
type="primary"
@click="handleSubmit(row)"
>提交</el-link
>
<el-link
v-if="hasPermission('invoice_receipt_view') && row.approvalStatus !== 'draft'"
type="primary"
@click="openFlow(row)"
>流程</el-link
>
<el-link
v-if="
hasPermission('invoice_receipt_approve') && row.approvalStatus === 'reviewing'
"
type="primary"
@click="handleApprove(row)"
>通过</el-link
>
<el-link
v-if="
hasPermission('invoice_receipt_approve') && row.approvalStatus === 'reviewing'
"
type="danger"
@click="handleReturn(row)"
>驳回</el-link
>
<el-link
v-if="hasPermission('invoice_receipt_void') && row.approvalStatus === 'approved'"
type="danger"
@click="handleVoid(row)"
>作废</el-link
>
</div>
</template>
</el-table-column>
</el-table>
<div class="receipt-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>
<el-dialog v-model="flowDialog.visible" title="审批流程" width="900px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="发票号码">
{{ flowDialog.row.invoiceNo || '-' }}
</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-table
v-loading="flowDialog.loading"
:data="flowDialog.records"
border
class="receipt-page__flow-table"
>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="actionName" label="操作" min-width="110" align="center" />
<el-table-column label="原状态" min-width="100" align="center">
<template #default="{ row }">{{ statusName(row.fromStatus) }}</template>
</el-table-column>
<el-table-column label="新状态" min-width="100" align="center">
<template #default="{ row }">{{ statusName(row.toStatus) }}</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作人" min-width="110" align="center" />
<el-table-column prop="createTime" label="操作时间" min-width="170" align="center" />
<el-table-column prop="reason" label="原因" min-width="160" show-overflow-tooltip />
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" />
</el-table>
</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/payment/invoiceReceipt';
import { invoiceReceiptTableColumns } from '@/option/payment/invoiceReceipt';
const emptyQuery = () => ({
invoiceNo: '',
invoiceDate: '',
projectName: '',
deptName: '',
approvalStatus: '',
kingdeeStatus: '',
});
export default {
name: 'InvoiceReceipt',
data() {
return {
ArrowDown,
ArrowUp,
Refresh,
query: emptyQuery(),
searchExpanded: false,
approvalStatusOptions: api.approvalStatusOptions,
kingdeeStatusOptions: api.kingdeeStatusOptions,
columns: invoiceReceiptTableColumns,
rows: [],
selection: [],
loading: false,
page: { current: 1, size: 10, total: 0 },
flowDialog: { visible: false, loading: false, row: {}, records: [] },
};
},
computed: {
...mapGetters(['permission']),
},
mounted() {
this.loadTable();
},
methods: {
hasPermission(code) {
return this.permission?.[code] !== false;
},
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async loadTable() {
this.loading = true;
try {
const data = this.unwrapData(
await api.getList(this.page.current, this.page.size, this.query)
);
this.rows = data.records || [];
this.page.total = Number(data.total || 0);
} finally {
this.loading = false;
}
},
handleSearch() {
this.page.current = 1;
this.loadTable();
},
resetSearch() {
this.query = emptyQuery();
this.handleSearch();
},
handleSizeChange() {
this.page.current = 1;
this.loadTable();
},
openCreate() {
this.$router.push({ path: '/payment/invoice-receipt/form', query: { mode: 'add' } });
},
openEdit(row) {
this.$router.push({
path: '/payment/invoice-receipt/form',
query: { mode: 'edit', id: row.id },
});
},
openView(row) {
this.$router.push({
path: '/payment/invoice-receipt/form',
query: { mode: 'view', id: row.id },
});
},
async openFlow(row) {
this.flowDialog.row = { ...row };
this.flowDialog.records = [];
this.flowDialog.visible = true;
this.flowDialog.loading = true;
try {
const detail = this.unwrapData(await api.getDetail(row.id));
this.flowDialog.row = { ...row, ...detail };
this.flowDialog.records = detail.records || [];
} finally {
this.flowDialog.loading = false;
}
},
async handleDelete(row) {
await this.$confirm('确认删除该收票登记?', '提示', { type: 'warning' });
await api.remove(row.id);
this.$message.success('删除成功');
this.loadTable();
},
async handleSubmit(row) {
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('请输入驳回原因', '审批驳回');
await api.returnBill({ id: row.id, reason: value });
this.$message.success('已驳回');
this.loadTable();
},
async handleVoid(row) {
const { value } = await this.$prompt('请输入作废原因', '作废收票登记');
await api.voidBill({ id: row.id, reason: value });
this.$message.success('作废成功');
this.loadTable();
},
async handleBatchSync() {
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
if (!rows.length) {
this.$message.warning('请选择至少一条审批通过的收票登记');
return;
}
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
this.$message.success(`已同步${rows.length}条收票登记`);
this.loadTable();
},
async handleExport() {
const data = this.unwrapData(await api.getList(1, 100000, this.query));
const exportRows = (data.records || []).map(item => ({
发票号: item.invoiceNo,
开票日期: item.invoiceDate,
所属项目: item.projectName,
所属组织: item.deptName,
付款方: item.payerName,
收款方: item.payeeName,
收票金额: Number(item.invoiceAmount || 0).toFixed(2),
审核状态: item.approvalStatusName,
当前节点: item.currentNode,
当前处理人: item.currentProcessor,
创建人: item.createUserName,
创建时间: item.createTime,
金蝶单据号: item.kingdeeBillNo,
金蝶单据状态: item.kingdeeStatusName,
}));
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`);
},
statusType(status) {
return (
{ approved: 'success', reviewing: 'warning', returned: 'danger', voided: 'info' }[status] ||
''
);
},
statusName(status) {
return (
{
draft: '草稿',
reviewing: '审批中',
approved: '审批通过',
returned: '已驳回',
voided: '已作废',
}[status] ||
status ||
'-'
);
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.receipt-page__search {
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.receipt-page__search-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px 24px;
}
.receipt-page__search :deep(.el-form-item) {
margin-bottom: 8px;
}
.receipt-page__search :deep(.el-input),
.receipt-page__search :deep(.el-select),
.receipt-page__search :deep(.el-date-editor) {
width: 100%;
}
.receipt-page__search-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 8px;
}
.receipt-page__table-panel {
margin-top: 8px;
}
.receipt-page__toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
}
.receipt-page__table-title {
font-size: 16px;
font-weight: 600;
}
.receipt-page__toolbar-actions,
.receipt-page__links {
display: flex;
align-items: center;
gap: 8px;
}
.receipt-page__links {
flex-wrap: wrap;
justify-content: center;
}
.receipt-page__pagination {
display: flex;
justify-content: flex-end;
padding: 16px 0;
}
.receipt-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.receipt-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
.receipt-page__flow-table {
margin-top: 16px;
}
@media (max-width: 1200px) {
.receipt-page__search-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
@@ -0,0 +1,852 @@
<template>
<basic-container class="payment-form-page">
<div class="payment-form-page__title">
{{ readonly ? '查看付款申请' : recordId ? '编辑付款申请' : '新增付款申请' }}
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="right"
label-width="auto"
class="payment-form-page__form"
>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">付款基本信息</div>
<el-row :gutter="24">
<el-col :span="12"
><el-form-item label="单据号"
><el-input
v-model="form.paymentNo"
disabled
placeholder="系统自动生成" /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="付款类型" prop="paymentType"
><el-select v-model="form.paymentType" :disabled="readonly" @change="handleTypeChange"
><el-option
v-for="item in paymentTypeOptions"
:key="item.value"
v-bind="item" /></el-select></el-form-item
></el-col>
<el-col v-if="form.paymentType !== 'project_advance'" :span="12"
><el-form-item label="结算单号" prop="referenceId"
><el-input
v-model="referenceLabel"
readonly
:disabled="readonly"
placeholder="请选择预结算/正式结算单"
><template #append
><el-button :disabled="readonly" @click="openReference">选择</el-button></template
></el-input
></el-form-item
></el-col
>
<el-col :span="12"
><el-form-item label="所属项目"
><el-input v-model="form.projectName" disabled
><template v-if="form.paymentType === 'project_advance'" #append
><el-button :disabled="readonly" @click="openReference">选择</el-button></template
></el-input
></el-form-item
></el-col
>
<el-col :span="12"
><el-form-item label="合同名称"
><el-input v-model="form.contractName" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="结算金额"
><el-input v-model="form.settlementAmount" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="可付款金额"
><el-input v-model="form.payableAmount" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="票款类型"
><el-input v-model="form.billType" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="付款比例" prop="paymentRatio"
><el-input-number
v-model="form.paymentRatio"
:disabled="readonly"
:min="0"
:max="100"
:precision="2"
:controls="false"
style="width: 100%" /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="申请付款金额" prop="appliedAmount"
><el-input-number
v-model="form.appliedAmount"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false"
style="width: 100%" /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="付款方式" prop="paymentMethod"
><el-select
v-model="form.paymentMethod"
:disabled="readonly"
@change="handlePaymentMethodChange"
><el-option
v-for="item in paymentMethodOptions"
:key="item.value"
v-bind="item" /></el-select></el-form-item
></el-col>
<el-col v-if="billPayment" :span="12"
><el-form-item label="汇票票据" prop="billLedgerId"
><el-select
v-model="form.billLedgerId"
:disabled="readonly"
filterable
remote
reserve-keyword
:remote-method="loadBillOptions"
:loading="billLoading"
placeholder="请选择可用汇票"
@change="handleBillChange"
><el-option
v-for="item in billOptions"
:key="item.id"
:label="`${item.billNo}|余额${formatMoney(item.availableBalance)}${
item.maturityDate
}`"
:value="item.id" /></el-select></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="收款方"
><el-input v-model="form.payeeName" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="收款账号"
><el-input v-model="form.bankAccount" :disabled="readonly" /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="开户银行"
><el-input v-model="form.bankName" :disabled="readonly" /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="申请人"
><el-input v-model="form.applicantName" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="申请日期"
><el-input v-model="form.applyDate" disabled /></el-form-item
></el-col>
<el-col :span="24"
><el-form-item label="备注"
><el-input
v-model="form.remark"
:disabled="readonly"
type="textarea"
maxlength="200"
show-word-limit /></el-form-item
></el-col>
</el-row>
</section>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">结算信息</div>
<el-table :data="settlementRows" border
><el-table-column type="index" label="序号" width="70" /><el-table-column
prop="settlementNo"
label="结算单号"
min-width="150" /><el-table-column
prop="settlementAmount"
label="结算总金额"
min-width="130" /><el-table-column
prop="payableAmount"
label="可付款金额"
min-width="130" /><el-table-column
prop="paymentRatio"
label="付款比例"
min-width="110" /><el-table-column
prop="appliedAmount"
label="付款金额"
min-width="130"
/></el-table>
</section>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">发票信息</div>
<div class="payment-form-page__section-actions">
<el-button type="primary" plain :disabled="readonly" @click="addInvoice"
>新增发票</el-button
>
</div>
<el-table :data="form.invoices" border
><el-table-column type="index" label="序号" width="70" /><el-table-column
prop="settlementNo"
label="结算单号"
min-width="120"
/><el-table-column label="发票号" min-width="140"
><template #default="{ row }"
><el-input v-model="row.invoiceNo" :disabled="readonly" /></template></el-table-column
><el-table-column label="开票日期" min-width="150"
><template #default="{ row }"
><el-date-picker
v-model="row.invoiceDate"
value-format="YYYY-MM-DD"
:disabled="readonly" /></template></el-table-column
><el-table-column label="发票类型" min-width="130"
><template #default="{ row }"
><el-input
v-model="row.invoiceType"
:disabled="readonly" /></template></el-table-column
><el-table-column label="税率" min-width="100"
><template #default="{ row }"
><el-input-number
v-model="row.taxRate"
:disabled="readonly"
:min="0"
:max="100"
:controls="false" /></template></el-table-column
><el-table-column label="发票金额(含税)" min-width="150"
><template #default="{ row }"
><el-input-number
v-model="row.invoiceAmount"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false" /></template></el-table-column
><el-table-column label="匹配金额(含税)" min-width="150"
><template #default="{ row }"
><el-input-number
v-model="row.matchedAmount"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false" /></template></el-table-column
><el-table-column label="操作" width="100"
><template #default="{ $index }"
><el-link v-if="!readonly" type="danger" @click="form.invoices.splice($index, 1)"
>删除</el-link
></template
></el-table-column
></el-table
>
</section>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">付款记录</div>
<el-table :data="paymentRecords" border
><el-table-column type="index" label="序号" width="70" /><el-table-column
prop="paidAmount"
label="付款金额"
min-width="160" /><el-table-column
prop="paidDate"
label="付款日期"
min-width="150" /><el-table-column
prop="paymentNo"
label="付款单号"
min-width="160" /><el-table-column
prop="kingdeeBillNo"
label="付款凭证"
min-width="160"
/></el-table>
</section>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">附件</div>
<vehicle-attachment-upload
v-model="form.attachments"
:readonly="readonly"
:multiple="true"
:limit="20"
:max-size="500"
:file-types="attachmentFileTypes"
:show-file-list="false"
button-text="上传附件"
@change="normalizeAttachments"
/><el-table :data="form.attachments" border
><el-table-column type="index" label="序号" width="70" /><el-table-column
label="附件类型"
width="160"
><template #default="{ row }"
><el-select v-model="row.attachmentType" :disabled="readonly"
><el-option label="磅单" value="weighing_slip" /><el-option
label="结算单"
value="settlement" /><el-option label="合同签章文件" value="contract" /><el-option
label="特批附件"
value="special_approval" /><el-option
label="其他"
value="other" /></el-select></template></el-table-column
><el-table-column prop="originalName" label="文件名" min-width="220" /><el-table-column
label="附件描述"
min-width="220"
><template #default="{ row }"
><el-input
v-model="row.description"
:disabled="readonly"
maxlength="200" /></template></el-table-column
><el-table-column prop="size" label="文件大小" width="120" /><el-table-column
prop="uploadUserName"
label="上传人"
width="130"
/><el-table-column prop="uploadTime" label="上传时间" width="170" /><el-table-column
label="操作"
width="100"
><template #default="{ row, $index }"
><el-link type="primary" :href="row.url || row.link" target="_blank">查看</el-link
><el-link v-if="!readonly" type="danger" @click="form.attachments.splice($index, 1)"
>删除</el-link
></template
></el-table-column
></el-table
>
</section>
<div class="payment-form-page__actions">
<el-button @click="goBack">返回</el-button
><el-button v-if="!readonly && canSave" @click="saveDraft">保存</el-button
><el-button
v-if="!readonly && canSave && hasPermission('payment_application_submit')"
type="primary"
@click="submitForm"
>提交</el-button
>
</div>
</el-form>
<el-dialog
v-model="referenceVisible"
:title="form.paymentType === 'project_advance' ? '选择项目合同' : '选择结算单'"
width="80%"
><el-table
v-if="form.paymentType === 'project_advance'"
:data="contractRows"
border
@row-click="selectContract"
><el-table-column prop="projectName" label="所属项目" /><el-table-column
prop="contractNo"
label="合同编号" /><el-table-column
prop="contractName"
label="合同名称" /><el-table-column prop="deptName" label="所属组织" /></el-table
><el-tabs v-else v-model="referenceTab"
><el-tab-pane label="正式结算单" name="formal"
><el-table :data="formalRows" border @row-click="selectFormal"
><el-table-column prop="formalSettlementNo" label="结算单号" /><el-table-column
prop="projectName"
label="所属项目" /><el-table-column
prop="contractName"
label="合同名称" /><el-table-column
prop="settlementAmount"
label="结算金额" /></el-table></el-tab-pane
><el-tab-pane label="预结算单" name="pre"
><el-table :data="preRows" border @row-click="selectPre"
><el-table-column prop="preSettlementNo" label="结算单号" /><el-table-column
prop="projectName"
label="所属项目" /><el-table-column
prop="contractName"
label="合同名称" /><el-table-column
prop="settlementAmount"
label="结算金额" /></el-table></el-tab-pane></el-tabs
></el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/paymentApplication';
import * as billLedgerApi from '@/api/payment/billLedger';
import * as formalApi from '@/api/settlement/formalSettlement';
import * as preApi from '@/api/settlement/preSettlement';
const emptyForm = () => ({
id: null,
paymentNo: '',
paymentType: 'project_advance',
settlementId: null,
preSettlementId: null,
projectId: null,
projectName: '',
deptId: null,
deptName: '',
contractId: null,
contractNo: '',
contractName: '',
payerName: '',
payeeName: '',
settlementAmount: 0,
payableAmount: 0,
billType: '',
paymentRatio: 50,
appliedAmount: 0,
paymentMethod: 'bank_transfer',
billLedgerId: null,
billNo: '',
receiptAccountId: null,
receiptAccountName: '',
bankName: '',
bankAccount: '',
applicantName: '',
applyDate: '',
remark: '',
attachments: [],
invoices: [],
});
export default {
name: 'PaymentApplicationForm',
data() {
return {
form: emptyForm(),
amountSyncing: false,
paymentRecords: [],
referenceVisible: false,
referenceTab: 'formal',
formalRows: [],
preRows: [],
contractRows: [],
billOptions: [],
billLoading: false,
paymentTypeOptions: api.paymentTypeOptions,
paymentMethodOptions: api.paymentMethodOptions,
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
'rar',
],
rules: {
paymentType: [{ required: true, message: '请选择付款类型' }],
referenceId: [{ validator: this.validateReference, trigger: 'change' }],
paymentMethod: [{ required: true, message: '请选择付款方式' }],
billLedgerId: [{ validator: this.validateBillLedger, trigger: 'change' }],
appliedAmount: [{ validator: this.validateAppliedAmount, trigger: 'change' }],
},
};
},
computed: {
...mapGetters(['permission']),
recordId() {
return this.$route.query.id || '';
},
readonly() {
return this.$route.query.mode === 'view';
},
canSave() {
const code = this.recordId ? 'payment_application_edit' : 'payment_application_add';
return this.hasPermission(code);
},
billPayment() {
return ['bank_draft', 'commercial_draft'].includes(this.form.paymentMethod);
},
referenceLabel() {
return this.form.settlementNo || this.form.preSettlementNo || '';
},
settlementRows() {
if (!this.referenceLabel) return [];
return [
{
settlementNo: this.referenceLabel,
settlementAmount: this.form.settlementAmount,
payableAmount: this.form.payableAmount,
paymentRatio: this.form.paymentRatio,
appliedAmount: this.form.appliedAmount,
},
];
},
},
watch: {
'form.paymentRatio'(value) {
if (this.amountSyncing || !Number(this.form.payableAmount)) return;
this.amountSyncing = true;
this.form.appliedAmount = Number(
((Number(this.form.payableAmount) * Number(value || 0)) / 100).toFixed(2)
);
this.$nextTick(() => {
this.amountSyncing = false;
});
},
'form.appliedAmount'(value) {
if (this.amountSyncing || !Number(this.form.payableAmount)) return;
this.amountSyncing = true;
this.form.paymentRatio = Number(
((Number(value || 0) / Number(this.form.payableAmount)) * 100).toFixed(2)
);
this.$nextTick(() => {
this.amountSyncing = false;
});
},
},
created() {
this.initialize();
},
methods: {
hasPermission(code) {
return this.permission?.[code] !== false;
},
validateReference(rule, value, callback) {
if (this.form.paymentType === 'progress_advance' && !this.form.preSettlementId) {
callback(new Error('请选择预结算单'));
return;
}
if (this.form.paymentType === 'settlement_payment' && !this.form.settlementId) {
callback(new Error('请选择正式结算单'));
return;
}
callback();
},
validateAppliedAmount(rule, value, callback) {
const amount = Number(value);
const payableAmount = Number(this.form.payableAmount || 0);
if (!Number.isFinite(amount) || amount < 0) {
callback(new Error('申请付款金额不能小于0'));
return;
}
if (payableAmount > 0 && amount > payableAmount) {
callback(new Error('申请付款金额不能超过可付款金额'));
return;
}
callback();
},
validateBillLedger(rule, value, callback) {
if (!this.billPayment) {
callback();
return;
}
if (!value) {
callback(new Error('请选择可用汇票'));
return;
}
const selected = this.billOptions.find(item => String(item.id) === String(value));
if (
selected &&
Number(this.form.appliedAmount || 0) > Number(selected.availableBalance || 0)
) {
callback(new Error('申请付款金额不能超过汇票可用余额'));
return;
}
callback();
},
validateInvoices() {
const appliedAmount = Number(this.form.appliedAmount || 0);
let matchedTotal = 0;
for (const invoice of this.form.invoices || []) {
const invoiceAmount = Number(invoice.invoiceAmount || 0);
const matchedAmount = Number(invoice.matchedAmount || 0);
if (invoiceAmount < 0 || matchedAmount < 0) {
throw new Error('发票金额和匹配金额不能小于0');
}
if (matchedAmount > invoiceAmount) {
throw new Error('单张发票匹配金额不能超过发票金额');
}
matchedTotal += matchedAmount;
}
if (matchedTotal > appliedAmount) {
throw new Error('发票匹配金额合计不能超过申请付款金额');
}
},
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async initialize() {
if (this.recordId) {
const data = this.unwrapData(await api.getDetail(this.recordId));
this.form = {
...emptyForm(),
...data,
attachments: this.parse(data.attachmentsJson),
invoices: data.invoices || [],
};
this.paymentRecords = data.paymentRecords || [];
if (this.billPayment) await this.loadBillOptions('', data.billLedgerId);
} else {
this.form.applyDate = this.$dayjs().format('YYYY-MM-DD');
this.form.applicantName =
this.$store.getters.userInfo?.realName || this.$store.getters.userInfo?.userName || '';
}
},
parse(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
try {
return JSON.parse(value) || [];
} catch {
return [];
}
},
handleTypeChange() {
if (this.form.paymentType === 'project_advance') {
this.form.settlementId = null;
this.form.preSettlementId = null;
}
},
async handlePaymentMethodChange() {
if (!this.billPayment) {
this.form.billLedgerId = null;
this.form.billNo = '';
this.billOptions = [];
return;
}
await this.loadBillOptions();
},
async loadBillOptions(keyword = '', selectedId = this.form.billLedgerId) {
if (!this.billPayment) return;
this.billLoading = true;
try {
this.billOptions =
this.unwrapData(
await billLedgerApi.getAvailableOptions(keyword, this.form.deptId, selectedId)
) || [];
} finally {
this.billLoading = false;
}
},
handleBillChange(id) {
const selected = this.billOptions.find(item => String(item.id) === String(id));
this.form.billNo = selected?.billNo || '';
this.$refs.formRef?.validateField('billLedgerId').catch(() => {});
},
async loadReferences() {
const [formalResponse, preResponse, contractResponse] = await Promise.all([
formalApi.getList(1, 100, { settlementType: 'payable', approvalStatus: 'approved' }),
preApi.getList(1, 100, { settlementType: 'payable', approvalStatus: 'approved' }),
formalApi.getContractOptions(''),
]);
const formal = this.unwrapData(formalResponse);
const pre = this.unwrapData(preResponse);
this.formalRows = formal.records || [];
this.preRows = pre.records || [];
this.contractRows = this.unwrapData(contractResponse) || [];
},
async openReference() {
await this.loadReferences();
this.referenceVisible = true;
},
selectContract(row) {
Object.assign(this.form, {
projectId: row.projectId,
projectName: row.projectName,
deptId: row.deptId,
deptName: row.deptName,
contractId: row.id,
contractNo: row.contractNo,
contractName: row.contractName,
payerName: row.payerName,
payeeName: row.payeeName,
billType: '项目预付',
});
this.referenceVisible = false;
if (this.billPayment) this.loadBillOptions();
},
selectFormal(row) {
Object.assign(this.form, {
settlementId: row.id,
preSettlementId: null,
settlementNo: row.formalSettlementNo,
projectName: row.projectName,
deptId: row.deptId,
deptName: row.deptName,
contractName: row.contractName,
settlementAmount: row.settlementAmount,
payableAmount: Math.max(
0,
Number(row.settlementAmount || 0) - Number(row.appliedPaymentAmount || 0)
),
billType: '正式结算单',
payeeName: row.payeeName,
});
this.referenceVisible = false;
if (this.billPayment) this.loadBillOptions();
},
selectPre(row) {
Object.assign(this.form, {
preSettlementId: row.id,
settlementId: null,
preSettlementNo: row.preSettlementNo,
projectName: row.projectName,
deptId: row.deptId,
deptName: row.deptName,
contractName: row.contractName,
settlementAmount: row.settlementAmount,
payableAmount: Math.max(
0,
Number(row.settlementAmount || 0) - Number(row.advanceAppliedAmount || 0)
),
billType: '预结算单',
payeeName: row.payeeName,
});
this.referenceVisible = false;
if (this.billPayment) this.loadBillOptions();
},
addInvoice() {
this.form.invoices.push({
settlementNo: this.referenceLabel,
invoiceNo: '',
invoiceDate: '',
invoiceType: '',
taxRate: 3,
invoiceAmount: 0,
matchedAmount: 0,
attachmentJson: '',
});
},
normalizeAttachments(files) {
const userName =
this.$store.getters.userInfo?.realName || this.$store.getters.userInfo?.userName || '';
const time = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.form.attachments = (files || []).map(file => ({
...file,
attachmentType: file.attachmentType || 'other',
description: file.description || '',
uploadUserName: file.uploadUserName || userName,
uploadTime: file.uploadTime || time,
}));
},
payload() {
const {
id,
paymentType,
settlementId,
preSettlementId,
projectId,
projectName,
deptId,
deptName,
contractId,
contractNo,
contractName,
payerName,
payeeName,
settlementAmount,
payableAmount,
billType,
paymentRatio,
appliedAmount,
paymentMethod,
billLedgerId,
billNo,
receiptAccountId,
receiptAccountName,
bankName,
bankAccount,
attachments,
remark,
invoices,
} = this.form;
return {
id,
paymentType,
settlementId,
preSettlementId,
projectId,
projectName,
deptId,
deptName,
contractId,
contractNo,
contractName,
payerName,
payeeName,
settlementAmount,
payableAmount,
billType,
paymentRatio,
appliedAmount,
paymentMethod,
billLedgerId,
billNo,
receiptAccountId,
receiptAccountName,
bankName,
bankAccount,
attachmentsJson: JSON.stringify(attachments || []),
remark,
invoices,
};
},
async saveDraft() {
await this.$refs.formRef.validate();
if (this.form.paymentType === 'project_advance' && !this.form.projectId) {
this.$message.warning('请选择所属项目');
return false;
}
try {
this.validateInvoices();
} catch (error) {
this.$message.warning(error.message);
return false;
}
const data = this.unwrapData(await api.save(this.payload()));
this.form.id = data;
this.$message.success('保存成功');
return true;
},
async submitForm() {
const saved = await this.saveDraft();
if (!saved) return;
await api.submit({ id: this.form.id });
this.$message.success('提交成功');
this.goBack();
},
goBack() {
this.$router.push('/payment/payment-application');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.payment-form-page__title,
.payment-form-page__section-title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.payment-form-page__title::before,
.payment-form-page__section-title::before {
width: 4px;
height: 20px;
margin-right: 8px;
background: #409eff;
content: '';
}
.payment-form-page__section {
margin-bottom: 24px;
}
.payment-form-page__section-title {
font-size: 16px;
}
.payment-form-page__section-actions {
display: flex;
justify-content: flex-end;
margin-bottom: 8px;
}
.payment-form-page__actions {
display: flex;
justify-content: center;
gap: 16px;
padding: 24px 0;
border-top: 1px solid #eff1f7;
}
.payment-form-page :deep(.el-form-item) {
margin-bottom: 16px;
}
.payment-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.payment-form-page :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
:deep(.payment-form-page.basic-container .basic-container__card > .el-card__body) {
padding: 0;
}
</style>
+476
View File
@@ -0,0 +1,476 @@
<template>
<basic-container class="payment-page">
<section class="payment-page__search">
<el-form :model="query" label-position="right" label-width="160px">
<div class="payment-page__search-grid">
<el-form-item label="单据号"
><el-input v-model="query.paymentNo" clearable
/></el-form-item>
<el-form-item label="日期"
><el-date-picker
v-model="query.applyDateRange"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="~"
start-placeholder="//"
end-placeholder="//"
/></el-form-item>
<el-form-item label="客户名称"
><el-input v-model="query.payeeName" clearable
/></el-form-item>
<el-form-item label="项目名称"
><el-input v-model="query.projectName" clearable
/></el-form-item>
<template v-if="searchExpanded">
<el-form-item label="所属组织"
><el-input v-model="query.deptName" clearable
/></el-form-item>
<el-form-item label="关联结算单"
><el-input v-model="query.settlementNo" clearable
/></el-form-item>
<el-form-item label="付款类型"
><el-select v-model="query.paymentType" clearable
><el-option
v-for="item in paymentTypeOptions"
:key="item.value"
v-bind="item" /></el-select
></el-form-item>
<el-form-item label="审核状态"
><el-select v-model="query.approvalStatus" clearable
><el-option
v-for="item in approvalStatusOptions"
:key="item.value"
v-bind="item" /></el-select
></el-form-item>
<el-form-item label="金蝶单据状态"
><el-select v-model="query.kingdeeStatus" clearable
><el-option
v-for="item in kingdeeStatusOptions"
:key="item.value"
v-bind="item" /></el-select
></el-form-item>
</template>
</div>
<div class="payment-page__search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-button text @click="searchExpanded = !searchExpanded"
><el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon
></el-button>
</div>
</el-form>
</section>
<section class="payment-page__table-panel">
<div class="payment-page__toolbar">
<div class="payment-page__toolbar-left">
<el-button
v-if="hasPermission('payment_application_add')"
type="primary"
@click="openCreate"
>新增付款申请</el-button
>
<el-button
v-if="hasPermission('payment_application_sync')"
type="primary"
plain
@click="handleSync"
>批量同步</el-button
>
<el-button
v-if="hasPermission('payment_application_add')"
type="primary"
plain
@click="openSettlementPayment"
>申请付尾款</el-button
>
<el-button
v-if="hasPermission('payment_application_add')"
type="primary"
plain
@click="openCreate"
>预付申请</el-button
>
<el-button
v-if="hasPermission('payment_application_export')"
type="primary"
plain
@click="handleExport"
>导出</el-button
>
</div>
<el-button text :icon="Refresh" @click="loadTable" />
</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>
<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="payment-page__links">
<el-link
v-if="hasPermission('payment_application_view')"
type="primary"
@click="openView(row)"
>查看</el-link
>
<el-link
v-if="
hasPermission('payment_application_edit') &&
['draft', 'returned'].includes(row.approvalStatus)
"
type="primary"
@click="openEdit(row)"
>编辑</el-link
>
<el-link
v-if="hasPermission('payment_application_submit') && row.approvalStatus === 'draft'"
type="primary"
@click="handleSubmit(row)"
>提交</el-link
>
<el-link
v-if="
hasPermission('payment_application_approve') && row.approvalStatus === 'reviewing'
"
type="primary"
@click="handleApprove(row)"
>通过</el-link
>
<el-link
v-if="
hasPermission('payment_application_approve') && row.approvalStatus === 'reviewing'
"
type="danger"
@click="handleReturn(row)"
>驳回</el-link
>
<el-link
v-if="hasPermission('payment_application_view') && row.approvalStatus !== 'draft'"
type="primary"
@click="openFlow(row)"
>流程</el-link
>
<el-link
v-if="
hasPermission('payment_application_void') && row.approvalStatus === 'approved'
"
type="danger"
@click="handleVoid(row)"
>作废</el-link
>
<el-link
v-if="hasPermission('payment_application_delete') && row.approvalStatus === 'draft'"
type="danger"
@click="handleDelete(row)"
>删除</el-link
>
</div></template
>
</el-table-column>
</el-table>
<div class="payment-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>
<el-dialog v-model="flowDialog.visible" title="审批流程" width="620px" append-to-body>
<el-descriptions :column="1" border>
<el-descriptions-item label="付款申请单号">
{{ flowDialog.row.paymentNo || '-' }}
</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 * as api from '@/api/payment/paymentApplication';
import { paymentApplicationTableColumns } from '@/option/payment/paymentApplication';
const emptyQuery = () => ({
paymentNo: '',
applyDateRange: [],
payeeName: '',
projectName: '',
deptName: '',
settlementNo: '',
paymentType: '',
approvalStatus: '',
kingdeeStatus: '',
});
export default {
name: 'PaymentApplication',
data() {
return {
ArrowDown,
ArrowUp,
Refresh,
query: emptyQuery(),
searchExpanded: false,
paymentTypeOptions: api.paymentTypeOptions,
approvalStatusOptions: api.approvalStatusOptions,
kingdeeStatusOptions: api.kingdeeStatusOptions,
columns: paymentApplicationTableColumns,
rows: [],
selection: [],
loading: false,
page: { current: 1, size: 10, total: 0 },
flowDialog: { visible: false, row: {} },
};
},
computed: {
...mapGetters(['permission']),
visibleSearchFields() {
return this.searchExpanded ? this.columns : this.columns.slice(0, 4);
},
},
mounted() {
this.loadTable();
},
methods: {
hasPermission(code) {
return this.permission?.[code] !== false;
},
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
buildParams() {
const params = { ...this.query };
const range = params.applyDateRange || [];
delete params.applyDateRange;
if (range.length === 2) {
params.applyStartDate = range[0];
params.applyEndDate = range[1];
}
return params;
},
async loadTable() {
this.loading = true;
try {
const data = this.unwrapData(
await api.getList(this.page.current, this.page.size, this.buildParams())
);
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();
},
handleSizeChange() {
this.page.current = 1;
this.loadTable();
},
openCreate() {
this.$router.push({ path: '/payment/payment-application/form', query: { mode: 'add' } });
},
openEdit(row) {
this.$router.push({
path: '/payment/payment-application/form',
query: { mode: 'edit', id: row.id },
});
},
openView(row) {
this.$router.push({
path: '/payment/payment-application/form',
query: { mode: 'view', id: row.id },
});
},
openSettlementPayment() {
this.openCreate();
},
selectedOne(action) {
if (this.selection.length !== 1) {
this.$message.warning(`${action}需选择一条付款申请`);
return null;
}
return this.selection[0];
},
async handleDelete(row) {
await this.$confirm('确认删除该付款申请?', '提示', { type: 'warning' });
await api.remove(row.id);
this.$message.success('删除成功');
this.loadTable();
},
async handleSubmit(row) {
await api.submit({ id: row.id });
this.$message.success('提交成功');
this.loadTable();
},
async handleApprove(row) {
await api.approve({ id: row.id });
this.$message.success('审批通过');
this.loadTable();
},
async handleReturn(row) {
const { value } = await this.$prompt('请输入驳回原因', '审批驳回');
await api.returnBill({ id: row.id, reason: value });
this.$message.success('已驳回');
this.loadTable();
},
openFlow(row) {
this.flowDialog.row = row;
this.flowDialog.visible = true;
},
async handleVoid(row) {
const { value } = await this.$prompt('请输入作废原因', '作废付款申请');
await api.voidBill({ id: row.id, reason: value });
this.$message.success('作废成功');
this.loadTable();
},
async handleSyncRow(row) {
const data = this.unwrapData(await api.syncKingdee(row.id));
this.$message.success(`已生成金蝶付款单:${data}`);
this.loadTable();
},
async handleSync() {
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
if (!rows.length) {
this.$message.warning('请选择至少一条审批通过的付款申请');
return;
}
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
this.$message.success(`已同步${rows.length}条付款申请`);
this.loadTable();
},
handleExport() {
const headers = this.columns.map(item => item.label);
const rows = this.rows.map(row => this.columns.map(item => row[item.prop] || ''));
const csv = [headers, ...rows]
.map(item => item.map(value => `"${String(value).replaceAll('"', '""')}"`).join(','))
.join('\n');
const blob = new Blob([`\ufeff${csv}`], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `付款申请${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.csv`;
link.click();
URL.revokeObjectURL(link.href);
},
statusType(status) {
return (
{ approved: 'success', reviewing: 'warning', returned: 'danger', voided: 'info' }[status] ||
''
);
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.payment-page__search {
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.payment-page__search-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px 24px;
}
.payment-page__search :deep(.el-form-item) {
margin-bottom: 8px;
}
.payment-page__search :deep(.el-input),
.payment-page__search :deep(.el-select),
.payment-page__search :deep(.el-date-editor) {
width: 100%;
}
.payment-page__search-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 8px;
}
.payment-page__table-panel {
margin-top: 8px;
}
.payment-page__toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
}
.payment-page__toolbar-left {
display: flex;
gap: 8px;
}
.payment-page__links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.payment-page__pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.payment-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.payment-page :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
:deep(.payment-page.basic-container .basic-container__card > .el-card__body) {
padding: 0;
}
@media (max-width: 1200px) {
.payment-page__search-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
.payment-page__search-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,369 @@
<template>
<basic-container class="claim-record-form-page">
<el-form :model="form" label-position="right" label-width="auto">
<section class="claim-record-form-page__section">
<div class="claim-record-form-page__section-title">收款信息</div>
<el-row :gutter="32">
<el-col :span="12">
<el-form-item label="认领通知单">
<el-input v-model="form.receiptNoticeNo" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="付款人"
><el-input v-model="form.payerName" disabled
/></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="收款金额">
<el-input :model-value="formatMoney(form.receiptAmount)" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交易时间">
<el-input v-model="form.transactionTime" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="对方户名">
<el-input v-model="form.counterpartyName" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="对方开户行">
<el-input v-model="form.counterpartyBank" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="对方账号">
<el-input v-model="form.counterpartyAccount" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="明细流水号">
<el-input v-model="form.detailSerialNo" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="已认领金额">
<el-input :model-value="formatMoney(form.claimAmount)" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="摘要"><el-input v-model="form.summary" disabled /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="关联结算单">
<el-input v-model="form.associatedSettlementNos" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="认领人">
<el-input v-model="form.claimerName" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="认领日期">
<el-input v-model="form.claimDate" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="认领人部门">
<el-input v-model="form.claimerDeptName" disabled />
</el-form-item>
</el-col>
</el-row>
</section>
<section class="claim-record-form-page__section">
<div class="claim-record-form-page__section-title">结算信息</div>
<el-table :data="form.settlements" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="结算单号" min-width="180" align="center">
<template #default="{ row }">
<el-link type="primary" @click="openSettlementDetail(row)">
{{ row.formalSettlementNo }}
</el-link>
</template>
</el-table-column>
<el-table-column label="结算总金额" min-width="150" align="right">
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
</el-table-column>
<el-table-column label="已认领收款金额" min-width="170" align="right">
<template #default="{ row }">{{ formatMoney(row.claimedReceiptAmount) }}</template>
</el-table-column>
<el-table-column label="分摊收款金额" min-width="170" align="right">
<template #default="{ row }">{{ formatMoney(row.allocatedReceiptAmount) }}</template>
</el-table-column>
</el-table>
</section>
<section class="claim-record-form-page__section">
<div class="claim-record-form-page__section-heading">
<div class="claim-record-form-page__section-title">附件信息</div>
<el-button
v-if="form.attachments.length"
type="primary"
:icon="Download"
@click="downloadAttachments"
>批量下载</el-button
>
</div>
<el-table :data="form.attachments" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="文件名" min-width="220">
<template #default="{ row }">{{ row.originalName || row.name || '-' }}</template>
</el-table-column>
<el-table-column prop="description" label="附件描述" min-width="240" />
<el-table-column label="文件大小" width="120">
<template #default="{ row }">{{ displayFileSize(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="120" align="center">
<template #default="{ row }">
<div class="claim-record-form-page__links">
<el-link v-if="row.url || row.link" type="primary" @click="viewAttachment(row)">
查看
</el-link>
<el-link
v-if="form.claimStatus === 'claimed'"
type="danger"
@click="removeAttachment(row)"
>删除</el-link
>
</div>
</template>
</el-table-column>
</el-table>
<vehicle-attachment-upload
v-if="form.claimStatus === 'claimed'"
v-model="form.attachments"
:multiple="true"
:limit="20"
:max-size="500"
:file-types="attachmentFileTypes"
:show-file-list="false"
button-text="上传附件"
class="claim-record-form-page__uploader"
@change="handleAttachmentChange"
/>
</section>
<section class="claim-record-form-page__section">
<div class="claim-record-form-page__section-title">备注</div>
<el-input v-model="form.remark" type="textarea" :rows="3" maxlength="200" disabled />
</section>
<div class="claim-record-form-page__actions">
<el-button @click="goBack">关闭</el-button>
</div>
</el-form>
</basic-container>
</template>
<script>
import { Download } from '@element-plus/icons-vue';
import * as api from '@/api/payment/receiptClaimRecord';
const emptyForm = () => ({
receiptNoticeNo: '',
payerName: '',
receiptAmount: 0,
transactionTime: '',
counterpartyName: '',
counterpartyBank: '',
counterpartyAccount: '',
detailSerialNo: '',
claimAmount: 0,
summary: '',
associatedSettlementNos: '',
claimerName: '',
claimDate: '',
claimerDeptName: '',
claimStatus: '',
settlements: [],
attachments: [],
remark: '',
});
export default {
name: 'ReceiptClaimRecordForm',
data() {
return {
Download,
form: emptyForm(),
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
],
};
},
computed: {
recordId() {
return this.$route.query.id || '';
},
},
created() {
this.initialize();
},
methods: {
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async initialize() {
if (!this.recordId) {
this.$message.error('缺少认领记录');
this.goBack();
return;
}
const data = this.unwrapData(await api.getDetail(this.recordId));
this.form = {
...emptyForm(),
...data,
settlements: data.settlements || [],
attachments: this.parseAttachments(data.attachmentsJson),
};
},
parseAttachments(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
try {
const result = JSON.parse(value);
return Array.isArray(result) ? result : [];
} catch (error) {
return [];
}
},
downloadAttachments() {
this.form.attachments.forEach(file => {
const url = file.url || file.link;
if (!url) return;
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = file.originalName || file.name || '';
anchor.target = '_blank';
anchor.click();
});
},
viewAttachment(row) {
window.open(row.url || row.link, '_blank', 'noopener');
},
async handleAttachmentChange(files) {
const userInfo = this.$store.getters.userInfo || {};
const userName = userInfo.realName || userInfo.userName || '';
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.form.attachments = (files || []).map(file => ({
...file,
description: file.description || '',
uploadUserName: file.uploadUserName || userName,
uploadTime: file.uploadTime || uploadTime,
}));
await this.persistAttachments('附件上传成功');
},
async removeAttachment(row) {
await this.$confirm('确定删除该附件吗?', '删除附件', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消',
});
this.form.attachments = this.form.attachments.filter(item => item !== row);
await this.persistAttachments('附件删除成功');
},
async persistAttachments(message) {
await api.updateAttachments({
id: this.recordId,
attachmentsJson: JSON.stringify(this.form.attachments || []),
});
this.$message.success(message);
},
openSettlementDetail(row) {
if (!row.formalSettlementId) return;
this.$router.push({
path: '/settlement/formal-settlement/form',
query: { mode: 'view', id: row.formalSettlementId, name: '查看正式结算' },
});
},
goBack() {
this.$router.push('/payment/receipt-claim-record');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
displayFileSize(size) {
if (size === null || size === undefined || size === '') return '-';
if (typeof size === 'string' && /[a-z]/i.test(size)) return size;
const bytes = Number(size);
if (!Number.isFinite(bytes)) return size;
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`;
return `${(bytes / 1024 / 1024).toFixed(2)}MB`;
},
},
};
</script>
<style scoped lang="scss">
.claim-record-form-page__section {
margin-bottom: 24px;
}
.claim-record-form-page__section-title {
display: flex;
align-items: center;
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
}
.claim-record-form-page__section-title::before {
width: 4px;
height: 20px;
margin-right: 8px;
background: #409eff;
content: '';
}
.claim-record-form-page__section-heading {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.claim-record-form-page__section-heading .claim-record-form-page__section-title {
margin-bottom: 0;
}
.claim-record-form-page :deep(.el-form-item) {
margin-bottom: 16px;
}
.claim-record-form-page__actions {
display: flex;
justify-content: center;
padding: 16px 0 8px;
}
.claim-record-form-page__links {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.claim-record-form-page__uploader {
margin-top: 12px;
}
.claim-record-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.claim-record-form-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
</style>
+294
View File
@@ -0,0 +1,294 @@
<template>
<basic-container class="claim-record-page">
<section class="claim-record-page__search">
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
<div class="claim-record-page__search-grid">
<el-form-item label="认领通知单">
<el-input v-model="query.receiptNoticeNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="对方户名">
<el-input v-model="query.counterpartyName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="对方开户行">
<el-input v-model="query.counterpartyBank" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="对方账号">
<el-input v-model="query.counterpartyAccount" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-show="searchExpanded" label="摘要">
<el-input v-model="query.summary" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-show="searchExpanded" label="状态">
<el-select v-model="query.claimStatus" clearable placeholder="请选择">
<el-option
v-for="item in claimStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-show="searchExpanded" label="交易日期">
<el-date-picker
v-model="query.transactionDateRange"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="~"
start-placeholder="开始日期"
end-placeholder="结束日期"
clearable
/>
</el-form-item>
<el-form-item v-show="searchExpanded" label="认领人">
<el-input v-model="query.claimerName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-show="searchExpanded" label="认领日期">
<el-date-picker
v-model="query.claimDateRange"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="~"
start-placeholder="开始日期"
end-placeholder="结束日期"
clearable
/>
</el-form-item>
<el-form-item v-show="searchExpanded" label="所属组织">
<el-input v-model="query.claimerDeptName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-show="searchExpanded" label="金蝶单据状态">
<el-select v-model="query.kingdeeBillStatus" clearable placeholder="请选择">
<el-option
v-for="item in kingdeeBillStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</div>
<div class="claim-record-page__search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-tooltip :content="searchExpanded ? '折叠' : '展开'" placement="top">
<el-button text @click="searchExpanded = !searchExpanded">
<el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon>
</el-button>
</el-tooltip>
</div>
</el-form>
</section>
<section class="claim-record-page__table-panel">
<el-table v-loading="loading" :data="rows" border>
<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="openDetail(row)">
{{ displayValue(row[column.prop]) }}
</el-link>
<el-tag v-else-if="column.status" :type="statusType(row.claimStatus)">
{{ displayValue(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="120" fixed="right" align="center">
<template #default="{ row }">
<el-link
v-if="hasPermission('receipt_claim_record_void') && row.claimStatus === 'claimed'"
type="danger"
@click="handleVoid(row)"
>作废</el-link
>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
<div class="claim-record-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>
</basic-container>
</template>
<script>
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/receiptClaimRecord';
import { receiptClaimRecordTableColumns } from '@/option/payment/receiptClaimRecord';
const emptyQuery = () => ({
receiptNoticeNo: '',
counterpartyName: '',
counterpartyBank: '',
counterpartyAccount: '',
summary: '',
claimStatus: '',
transactionDateRange: [],
claimerName: '',
claimDateRange: [],
claimerDeptName: '',
kingdeeBillStatus: '',
});
export default {
name: 'ReceiptClaimRecord',
data() {
return {
ArrowDown,
ArrowUp,
query: emptyQuery(),
searchExpanded: false,
claimStatusOptions: api.claimStatusOptions,
kingdeeBillStatusOptions: api.kingdeeBillStatusOptions,
columns: receiptClaimRecordTableColumns,
rows: [],
loading: false,
page: { current: 1, size: 10, total: 0 },
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo?.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
},
},
mounted() {
this.loadTable();
},
methods: {
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
queryParams() {
const transactionRange = this.query.transactionDateRange || [];
const claimRange = this.query.claimDateRange || [];
return {
...this.query,
transactionDateRange: undefined,
claimDateRange: undefined,
transactionStartDate: transactionRange[0],
transactionEndDate: transactionRange[1],
claimStartDate: claimRange[0],
claimEndDate: claimRange[1],
};
},
async loadTable() {
this.loading = true;
try {
const data = this.unwrapData(
await api.getList(this.page.current, this.page.size, this.queryParams())
);
this.rows = data.records || [];
this.page.total = Number(data.total || 0);
} finally {
this.loading = false;
}
},
handleSearch() {
this.page.current = 1;
this.loadTable();
},
resetSearch() {
this.query = emptyQuery();
this.handleSearch();
},
handleSizeChange() {
this.page.current = 1;
this.loadTable();
},
openDetail(row) {
this.$router.push({ path: '/payment/receipt-claim-record/form', query: { id: row.id } });
},
async handleVoid(row) {
await this.$confirm(
'作废后将回退本次认领金额,并生成审核通过状态的金蝶认领冲单,是否继续?',
'作废认领记录',
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }
);
const kingdeeBillNo = this.unwrapData(await api.voidClaim(row.id));
this.$message.success(`作废成功,金蝶冲单号:${kingdeeBillNo}`);
await this.loadTable();
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission?.[code], false);
},
statusType(status) {
return status === 'voided' ? 'info' : 'success';
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.claim-record-page__search {
padding: 12px 12px 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.claim-record-page__search-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: 16px;
row-gap: 8px;
}
.claim-record-page__search :deep(.el-form-item) {
margin-bottom: 0;
}
.claim-record-page__search :deep(.el-input),
.claim-record-page__search :deep(.el-select),
.claim-record-page__search :deep(.el-date-editor) {
width: 100%;
}
.claim-record-page__search-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 8px;
}
.claim-record-page__table-panel {
margin-top: 8px;
}
.claim-record-page__pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.claim-record-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.claim-record-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
@media (max-width: 1200px) {
.claim-record-page__search-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
+537
View File
@@ -0,0 +1,537 @@
<template>
<basic-container class="receipt-claim-form-page">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="right"
label-width="auto"
class="receipt-claim-form-page__form"
>
<section class="receipt-claim-form-page__section">
<div class="receipt-claim-form-page__section-title">收款信息</div>
<el-row :gutter="32">
<el-col :span="12"
><el-form-item label="认领通知单"
><el-input v-model="form.receiptNoticeNo" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="付款人"
><el-input v-model="form.payerName" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="收款金额"
><el-input :model-value="formatMoney(form.receiptAmount)" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="交易时间"
><el-input v-model="form.transactionTime" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="对方户名"
><el-input v-model="form.counterpartyName" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="对方开户行"
><el-input v-model="form.counterpartyBank" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="对方账号"
><el-input v-model="form.counterpartyAccount" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="明细流水号"
><el-input v-model="form.detailSerialNo" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="已认领金额"
><el-input :model-value="formatMoney(form.claimedAmount)" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="摘要"><el-input v-model="form.summary" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="关联结算单" prop="settlementIds"
><el-input
:model-value="settlementLabel"
readonly
placeholder="请选择已审批通过的应收正式结算单"
><template #append
><el-button
:icon="Search"
title="选择结算单"
@click="openSettlementDialog" /></template></el-input></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="认领人"
><el-input v-model="form.claimerName" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="认领日期"
><el-input v-model="form.claimDate" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="认领人部门"
><el-input v-model="form.claimerDeptName" disabled /></el-form-item
></el-col>
</el-row>
</section>
<section class="receipt-claim-form-page__section">
<div class="receipt-claim-form-page__section-title">结算信息</div>
<el-table :data="form.settlements" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="结算单号" min-width="180" align="center"
><template #default="{ row }"
><el-link type="primary" @click="openSettlementDetail(row)">{{
row.formalSettlementNo
}}</el-link></template
></el-table-column
>
<el-table-column label="结算总金额" min-width="150" align="right"
><template #default="{ row }">{{
formatMoney(row.settlementAmount)
}}</template></el-table-column
>
<el-table-column label="已认领收款金额" min-width="160" align="right"
><template #default="{ row }">{{
formatMoney(row.claimedReceiptAmount)
}}</template></el-table-column
>
<el-table-column label="分摊收款金额" min-width="180"
><template #default="{ row }"
><el-input-number
v-model="row.allocatedReceiptAmount"
:min="0"
:max="remainingAmount(row)"
:precision="2"
:controls="false" /></template
></el-table-column>
</el-table>
</section>
<section class="receipt-claim-form-page__section">
<div class="receipt-claim-form-page__section-heading">
<div class="receipt-claim-form-page__section-title">附件信息</div>
<el-button
v-if="form.attachments.length"
type="primary"
plain
:icon="Download"
@click="downloadAttachments"
>批量下载</el-button
>
</div>
<el-table :data="form.attachments" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="文件名" min-width="220"
><template #default="{ row }">{{
row.originalName || row.name || '-'
}}</template></el-table-column
>
<el-table-column label="附件描述" min-width="240"
><template #default="{ row }"
><el-input v-model="row.description" maxlength="200" /></template
></el-table-column>
<el-table-column prop="size" label="文件大小" width="120" /><el-table-column
prop="uploadUserName"
label="上传人"
width="140"
/><el-table-column prop="uploadTime" label="上传时间" width="170" />
<el-table-column label="操作" width="120" align="center"
><template #default="{ row, $index }"
><div class="receipt-claim-form-page__links">
<el-link type="primary" :href="row.url || row.link" target="_blank">查看</el-link
><el-link type="danger" @click="form.attachments.splice($index, 1)">删除</el-link>
</div></template
></el-table-column
>
</el-table>
<vehicle-attachment-upload
v-model="form.attachments"
:multiple="true"
:limit="20"
:max-size="500"
:file-types="attachmentFileTypes"
:show-file-list="false"
button-text="上传附件"
class="receipt-claim-form-page__uploader"
@change="normalizeAttachments"
/>
</section>
<section class="receipt-claim-form-page__section">
<div class="receipt-claim-form-page__section-title">备注</div>
<el-form-item class="receipt-claim-form-page__remark"
><el-input
v-model="form.remark"
type="textarea"
:rows="3"
maxlength="200"
show-word-limit
/></el-form-item>
</section>
<div class="receipt-claim-form-page__actions">
<el-button type="primary" :loading="submitting" @click="submitClaim">确认</el-button
><el-button :disabled="submitting" @click="goBack">取消</el-button>
</div>
</el-form>
<el-dialog
v-model="settlementDialog.visible"
title="选择应收正式结算单"
width="86%"
append-to-body
>
<div class="receipt-claim-form-page__dialog-search">
<el-input
v-model="settlementDialog.keyword"
clearable
placeholder="结算单号、项目或合同"
@keyup.enter="loadSettlementCandidates"
/><el-button type="primary" @click="loadSettlementCandidates">查询</el-button>
</div>
<el-table
v-loading="settlementDialog.loading"
:data="settlementDialog.rows"
border
@selection-change="settlementDialog.selection = $event"
>
<el-table-column type="selection" width="52" align="center" /><el-table-column
prop="formalSettlementNo"
label="结算单号"
min-width="170"
/><el-table-column prop="projectName" label="所属项目" min-width="150" /><el-table-column
prop="deptName"
label="所属组织"
min-width="150"
/><el-table-column prop="payerName" label="付款方" min-width="150" /><el-table-column
prop="payeeName"
label="收款方"
min-width="150"
/><el-table-column label="结算总金额" min-width="140" align="right"
><template #default="{ row }">{{
formatMoney(row.settlementAmount)
}}</template></el-table-column
><el-table-column label="已认领收款金额" min-width="150" align="right"
><template #default="{ row }">{{
formatMoney(row.claimedReceiptAmount)
}}</template></el-table-column
>
</el-table>
<template #footer
><el-button @click="settlementDialog.visible = false">取消</el-button
><el-button type="primary" @click="confirmSettlements">确定</el-button></template
>
</el-dialog>
</basic-container>
</template>
<script>
import { Download, Search } from '@element-plus/icons-vue';
import * as api from '@/api/payment/receiptFlow';
const emptyForm = () => ({
id: null,
receiptNoticeNo: '',
payerName: '',
receiptAmount: 0,
counterpartyName: '',
counterpartyAccount: '',
counterpartyBank: '',
summary: '',
transactionTime: '',
detailSerialNo: '',
claimedAmount: 0,
claimerName: '',
claimerDeptName: '',
claimDate: '',
settlementIds: [],
settlements: [],
attachments: [],
remark: '',
});
export default {
name: 'ReceiptFlowForm',
data() {
return {
Download,
Search,
form: emptyForm(),
submitting: false,
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
],
settlementDialog: { visible: false, loading: false, keyword: '', rows: [], selection: [] },
rules: {
settlementIds: [
{ required: true, message: '请选择应收正式结算单', trigger: 'change' },
{ validator: this.validateSettlements, trigger: 'change' },
],
},
};
},
computed: {
flowId() {
return this.$route.query.id || '';
},
settlementLabel() {
return this.form.settlements
.map(item => item.formalSettlementNo)
.filter(Boolean)
.join(',');
},
allocatedTotal() {
return this.form.settlements.reduce(
(total, item) => total + Number(item.allocatedReceiptAmount || 0),
0
);
},
},
created() {
this.initialize();
},
methods: {
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async initialize() {
if (!this.flowId) {
this.$message.error('缺少收款流水');
this.goBack();
return;
}
const data = this.unwrapData(await api.getDetail(this.flowId));
const userInfo = this.$store.getters.userInfo || {};
this.form = {
...emptyForm(),
...data,
claimerName: userInfo.realName || userInfo.userName || data.claimerName || '',
claimerDeptName: userInfo.deptName || userInfo.dept_name || data.claimerDeptName || '',
claimDate: this.$dayjs().format('YYYY-MM-DD'),
settlementIds: [],
settlements: [],
attachments: [],
};
},
validateSettlements(rule, value, callback) {
this.form.settlements.length ? callback() : callback(new Error('请选择应收正式结算单'));
},
async openSettlementDialog() {
this.settlementDialog.visible = true;
await this.loadSettlementCandidates();
},
async loadSettlementCandidates() {
this.settlementDialog.loading = true;
try {
this.settlementDialog.rows =
this.unwrapData(
await api.getSettlementCandidates(this.settlementDialog.keyword, this.flowId)
) || [];
} finally {
this.settlementDialog.loading = false;
}
},
confirmSettlements() {
const rows = this.settlementDialog.selection;
if (!rows.length) {
this.$message.warning('请至少选择一张应收正式结算单');
return;
}
const first = rows[0];
if (
rows.some(
item =>
String(item.projectId) !== String(first.projectId) ||
String(item.deptId) !== String(first.deptId) ||
item.payerName !== first.payerName ||
item.payeeName !== first.payeeName
)
) {
this.$message.warning('关联结算单必须属于同一项目、组织及收付款方');
return;
}
if (
this.form.counterpartyName &&
rows.some(item => item.payerName && item.payerName !== this.form.counterpartyName)
) {
this.$message.warning('对方户名与结算单付款方不一致');
return;
}
this.form.settlements = rows.map(item => ({
...item,
formalSettlementId: item.id,
settlementId: item.id,
allocatedReceiptAmount: 0,
}));
this.form.settlementIds = this.form.settlements.map(item => item.formalSettlementId);
this.settlementDialog.visible = false;
this.$nextTick(() => this.$refs.formRef?.validateField('settlementIds'));
},
remainingAmount(row) {
return Math.max(Number(row.settlementAmount || 0) - Number(row.claimedReceiptAmount || 0), 0);
},
normalizeAttachments(files) {
const userInfo = this.$store.getters.userInfo || {};
const userName = userInfo.realName || userInfo.userName || '';
const time = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.form.attachments = (files || []).map(file => ({
...file,
description: file.description || '',
uploadUserName: file.uploadUserName || userName,
uploadTime: file.uploadTime || time,
}));
},
downloadAttachments() {
this.form.attachments.forEach(file => {
const url = file.url || file.link;
if (!url) return;
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = file.originalName || file.name || '';
anchor.target = '_blank';
anchor.click();
});
},
async submitClaim() {
await this.$refs.formRef.validate();
const amount = Number(this.form.receiptAmount || 0);
if (amount <= 0 || this.allocatedTotal <= 0) {
this.$message.warning('分摊收款金额必须大于0');
return;
}
if (this.allocatedTotal > amount - Number(this.form.claimedAmount || 0) + 0.001) {
this.$message.warning('本次分摊金额不能超过流水剩余可认领金额');
return;
}
for (const row of this.form.settlements)
if (Number(row.allocatedReceiptAmount || 0) > this.remainingAmount(row) + 0.001) {
this.$message.warning(`结算单${row.formalSettlementNo}的累计认领金额不能超过结算总金额`);
return;
}
this.submitting = true;
try {
await api.claim({
flowId: this.flowId,
attachmentsJson: JSON.stringify(this.form.attachments || []),
remark: this.form.remark,
settlements: this.form.settlements.map(item => ({
settlementId: item.formalSettlementId || item.settlementId,
allocatedReceiptAmount: item.allocatedReceiptAmount,
})),
});
this.$message.success('认领成功');
this.goBack();
} finally {
this.submitting = false;
}
},
openSettlementDetail(row) {
const id = row.formalSettlementId || row.settlementId || row.id;
if (id)
this.$router.push({
path: '/settlement/formal-settlement/form',
query: { mode: 'view', id, name: '查看正式结算' },
});
},
goBack() {
this.$router.push('/payment/receipt-flow');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.receipt-claim-form-page__section-title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.receipt-claim-form-page__section-title::before {
width: 4px;
height: 20px;
margin-right: 8px;
background: #409eff;
content: '';
}
.receipt-claim-form-page__section {
margin-bottom: 24px;
}
.receipt-claim-form-page__section-title {
font-size: 16px;
}
.receipt-claim-form-page__section-heading {
display: flex;
align-items: center;
justify-content: space-between;
}
.receipt-claim-form-page__section-heading .receipt-claim-form-page__section-title {
margin-bottom: 0;
}
.receipt-claim-form-page__form :deep(.el-form-item) {
margin-bottom: 16px;
}
.receipt-claim-form-page__form :deep(.el-select),
.receipt-claim-form-page__form :deep(.el-input-number) {
width: 100%;
}
.receipt-claim-form-page__links {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.receipt-claim-form-page__remark :deep(.el-form-item__content) {
margin-left: 0 !important;
}
.receipt-claim-form-page__uploader {
margin-top: 12px;
}
.receipt-claim-form-page__actions {
display: flex;
justify-content: center;
gap: 8px;
padding: 16px 0 8px;
}
.receipt-claim-form-page__dialog-search {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 12px;
}
.receipt-claim-form-page__dialog-search .el-input {
width: 360px;
}
.receipt-claim-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.receipt-claim-form-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
@media (max-width: 900px) {
.receipt-claim-form-page__form :deep(.el-col-12) {
max-width: 100%;
flex: 0 0 100%;
}
}
</style>
+269
View File
@@ -0,0 +1,269 @@
<template>
<basic-container class="receipt-flow-page">
<section class="receipt-flow-page__search">
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
<div class="receipt-flow-page__search-grid">
<el-form-item label="认领通知单">
<el-input v-model="query.receiptNoticeNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="对方户名">
<el-input v-model="query.counterpartyName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="对方开户行">
<el-input v-model="query.counterpartyBank" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="对方账号">
<el-input v-model="query.counterpartyAccount" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-show="searchExpanded" label="摘要">
<el-input v-model="query.summary" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-show="searchExpanded" label="认领状态">
<el-select v-model="query.claimStatus" clearable placeholder="请选择">
<el-option
v-for="item in claimStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-show="searchExpanded" label="交易时间">
<el-date-picker
v-model="query.transactionTimeRange"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
range-separator="~"
start-placeholder="开始时间"
end-placeholder="结束时间"
clearable
/>
</el-form-item>
</div>
<div class="receipt-flow-page__search-actions">
<el-button @click="searchExpanded = !searchExpanded">
{{ searchExpanded ? '折叠' : '展开' }}
</el-button>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</div>
</el-form>
</section>
<section class="receipt-flow-page__table-panel">
<div class="receipt-flow-page__toolbar">
<div />
<el-button
v-if="hasPermission('receipt_flow_sync')"
type="primary"
:loading="syncing"
@click="handleSync"
>
手动同步流水
</el-button>
</div>
<el-table v-loading="loading" :data="rows" border>
<el-table-column type="index" label="序号" width="64" 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 && canClaim(row)" type="primary" @click="openClaim(row)">
{{ displayValue(row[column.prop]) }}
</el-link>
<span v-else-if="column.link">{{ displayValue(row[column.prop]) }}</span>
<el-tag v-else-if="column.status" :type="statusType(row.claimStatus)">
{{ displayValue(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="120" fixed="right" align="center">
<template #default="{ row }">
<el-link v-if="canClaim(row)" type="primary" @click="openClaim(row)">认领</el-link>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
<div class="receipt-flow-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>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/receiptFlow';
import { receiptFlowTableColumns } from '@/option/payment/receiptFlow';
const emptyQuery = () => ({
receiptNoticeNo: '',
counterpartyName: '',
counterpartyBank: '',
counterpartyAccount: '',
summary: '',
claimStatus: '',
transactionTimeRange: [],
});
export default {
name: 'ReceiptFlow',
data() {
return {
query: emptyQuery(),
claimStatusOptions: api.claimStatusOptions,
columns: receiptFlowTableColumns,
rows: [],
loading: false,
syncing: false,
searchExpanded: false,
page: { current: 1, size: 10, total: 0 },
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo?.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
},
},
mounted() {
this.loadTable();
},
methods: {
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async loadTable() {
this.loading = true;
try {
const range = this.query.transactionTimeRange || [];
const data = this.unwrapData(
await api.getList(this.page.current, this.page.size, {
...this.query,
transactionTimeRange: undefined,
transactionStartTime: range[0],
transactionEndTime: range[1],
})
);
this.rows = data.records || [];
this.page.total = Number(data.total || 0);
} finally {
this.loading = false;
}
},
handleSearch() {
this.page.current = 1;
this.loadTable();
},
resetSearch() {
this.query = emptyQuery();
this.handleSearch();
},
handleSizeChange() {
this.page.current = 1;
this.loadTable();
},
async handleSync() {
this.syncing = true;
try {
const syncedCount = Number(this.unwrapData(await api.sync()) || 0);
this.$message.success(`流水同步完成,共同步${syncedCount}`);
await this.loadTable();
} finally {
this.syncing = false;
}
},
openClaim(row) {
this.$router.push({
path: '/payment/receipt-flow/form',
query: { mode: 'add', id: row.id },
});
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission?.[code], false);
},
canClaim(row) {
return this.hasPermission('receipt_flow_claim') && Number(row.remainingAmount || 0) > 0;
},
statusType(status) {
return { claimed: 'success', partial: 'warning', unclaimed: 'info' }[status] || '';
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.receipt-flow-page__search {
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.receipt-flow-page__search-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px 24px;
}
.receipt-flow-page__search :deep(.el-form-item) {
margin-bottom: 8px;
}
.receipt-flow-page__search :deep(.el-input),
.receipt-flow-page__search :deep(.el-select),
.receipt-flow-page__search :deep(.el-date-editor) {
width: 100%;
}
.receipt-flow-page__search-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 8px;
}
.receipt-flow-page__table-panel {
margin-top: 8px;
}
.receipt-flow-page__toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
}
.receipt-flow-page__pagination {
display: flex;
justify-content: flex-end;
padding: 16px 0;
}
.receipt-flow-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.receipt-flow-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
@media (max-width: 1200px) {
.receipt-flow-page__search-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
@@ -1,5 +1,9 @@
<template>
<el-dialog v-model="visible" :title="title" width="96%" top="2vh" append-to-body destroy-on-close>
<component
:is="editorContainer"
v-bind="editorContainerProps"
@update:model-value="visible = $event"
>
<div v-loading="loading" class="formal-editor">
<section-card title="结算基本信息">
<el-form
@@ -164,15 +168,27 @@
</el-table>
</section-card>
</div>
<template #footer>
<template v-if="!pageMode" #footer>
<el-button @click="visible = false">取消</el-button>
<el-button v-if="editable" type="primary" :loading="saving" @click="handleSave"
>提交</el-button
>
</template>
<div v-if="pageMode" class="formal-editor__page-actions">
<el-button @click="visible = false">取消</el-button>
<el-button v-if="editable" type="primary" :loading="saving" @click="handleSave">
提交
</el-button>
</div>
<el-dialog v-model="candidate.visible" title="选择预结算单" width="88%" append-to-body>
<el-form :model="candidate.query" inline label-position="right" label-width="160px" class="formal-editor__candidate-search">
<el-form
:model="candidate.query"
inline
label-position="right"
label-width="160px"
class="formal-editor__candidate-search"
>
<el-form-item label="预结算单号"
><el-input v-model="candidate.query.preSettlementNo" clearable
/></el-form-item>
@@ -222,7 +238,13 @@
</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" class="formal-editor__detail-search">
<el-form
:model="detailCandidate.query"
inline
label-position="right"
label-width="160px"
class="formal-editor__detail-search"
>
<el-form-item label="批次号"
><el-input v-model="detailCandidate.query.batchNo" clearable
/></el-form-item>
@@ -344,7 +366,7 @@
></template
>
</el-dialog>
</el-dialog>
</component>
</template>
<script>
@@ -371,7 +393,16 @@ import {
export default {
name: 'FormalSettlementEditor',
props: { modelValue: Boolean, recordId: [String, Number], readonly: Boolean },
props: {
modelValue: Boolean,
recordId: [String, Number],
readonly: Boolean,
pageMode: Boolean,
initialData: {
type: Object,
default: null,
},
},
emits: ['update:modelValue', 'success'],
data() {
return {
@@ -446,6 +477,22 @@ export default {
this.$emit('update:modelValue', value);
},
},
editorContainer() {
return this.pageMode ? 'div' : 'el-dialog';
},
editorContainerProps() {
if (this.pageMode) {
return { class: 'formal-editor-shell formal-editor-shell--page' };
}
return {
modelValue: this.visible,
title: this.title,
width: '96%',
top: '2vh',
appendToBody: true,
destroyOnClose: true,
};
},
editable() {
return !this.readonly;
},
@@ -454,8 +501,11 @@ export default {
},
},
watch: {
modelValue(value) {
if (value) this.initialize();
modelValue: {
immediate: true,
handler(value) {
if (value) this.initialize();
},
},
},
methods: {
@@ -468,6 +518,7 @@ export default {
await this.loadContracts();
if (!this.recordId) {
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
if (this.initialData) this.applyInitialData();
return;
}
this.loading = true;
@@ -490,6 +541,55 @@ export default {
this.loading = false;
}
},
applyInitialData() {
const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : [];
if (!rows.length) return;
const first = rows[0];
const settlementType = this.initialData.settlementType || first.settlementType || 'payable';
const contractId = first.contractId || null;
if (!this.contracts.some(item => String(item.id) === String(contractId))) {
this.contracts.push({
id: contractId,
contractNo: first.contractNo,
contractName: first.contractName,
projectId: first.projectId,
projectName: first.projectName,
deptId: first.deptId,
deptName: first.deptName,
payerName: first.payerName,
payeeName: first.payeeName,
settlementType,
});
}
this.handleContractChange(contractId);
Object.assign(this.form, {
contractId,
contractNo: first.contractNo || this.form.contractNo,
contractName: first.contractName || this.form.contractName,
projectId: first.projectId || this.form.projectId,
projectName: first.projectName || this.form.projectName,
deptId: first.deptId || this.form.deptId,
deptName: first.deptName || this.form.deptName,
payerName: first.payerName || this.form.payerName,
payeeName: first.payeeName || this.form.payeeName,
settlementType,
settlementTypeName: settlementType === 'receivable' ? '应收' : '应付',
currency: first.currency || 'RMB',
});
this.details = rows.map(row => ({
...row,
sourceDetailId: row.sourceDetailId || row.id,
settlementAmountTax:
row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount,
}));
this.form.sourceDetailIds = this.details.map(item => item.sourceDetailId);
this.form.settlementAmount = this.details.reduce(
(total, item) => total + Number(item.settlementAmountTax || 0),
0
);
this.form.localSettlementAmount =
this.form.settlementAmount * Number(this.form.exchangeRate || 1);
},
async loadContracts(keyword = '') {
const response = await getContractOptions(keyword);
this.contracts = this.unwrapData(response) || [];
@@ -497,7 +597,9 @@ export default {
handleContractChange(id) {
const contract = this.contracts.find(item => String(item.id) === String(id));
if (!contract) return;
const formalSettlementId = this.form.id;
Object.assign(this.form, contract, {
id: formalSettlementId,
contractId: contract.id,
settlementTypeName: contract.settlementType === 'receivable' ? '应收' : '应付',
});
@@ -724,6 +826,13 @@ export default {
display: flex;
gap: 8px;
}
.formal-editor__page-actions {
display: flex;
justify-content: flex-end;
padding: 16px 0 4px;
border-top: 1px solid #eff1f7;
gap: 8px;
}
.formal-editor__links {
display: flex;
flex-wrap: wrap;
@@ -1,86 +1,82 @@
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="96%"
top="2vh"
append-to-body
destroy-on-close
class="pre-settlement-editor"
<component
:is="editorContainer"
v-bind="editorContainerProps"
@update:model-value="visible = $event"
@closed="handleClosed"
>
<div v-loading="loading" class="pre-settlement-editor__content">
<section-card title="结算基本信息">
<el-form
ref="formRef"
:model="form"
:rules="formRules"
label-position="right"
label-width="auto"
class="pre-settlement-editor__form"
>
<el-row :gutter="24">
<el-col v-for="field in formFields" :key="field.prop" :span="field.span || 8">
<el-form-item :label="field.label" :prop="field.prop">
<el-select
v-if="field.type === 'contract'"
v-model="form.contractId"
filterable
remote
clearable
:remote-method="loadContractOptions"
:loading="contractLoading"
:disabled="!editable || details.length > 0"
placeholder="请选择合同"
@change="handleContractChange"
>
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="`${item.contractName}${item.contractNo || '-'}`"
:value="item.id"
ref="formRef"
:model="form"
:rules="formRules"
label-position="right"
label-width="auto"
class="pre-settlement-editor__form"
>
<el-row :gutter="24">
<el-col v-for="field in formFields" :key="field.prop" :span="field.span || 8">
<el-form-item :label="field.label" :prop="field.prop">
<el-select
v-if="field.type === 'contract'"
v-model="form.contractId"
filterable
remote
clearable
:remote-method="loadContractOptions"
:loading="contractLoading"
:disabled="!editable || details.length > 0"
placeholder="请选择合同"
@change="handleContractChange"
>
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="`${item.contractName}${item.contractNo || '-'}`"
:value="item.id"
/>
</el-select>
<el-date-picker
v-else-if="field.type === 'date'"
v-model="form[field.prop]"
type="date"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
:disabled="!editable || form.currency === 'RMB'"
placeholder="请选择"
@change="recalculateLocalAmount"
/>
</el-select>
<el-date-picker
v-else-if="field.type === 'date'"
v-model="form[field.prop]"
type="date"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
:disabled="!editable || form.currency === 'RMB'"
placeholder="请选择"
@change="recalculateLocalAmount"
/>
<el-input-number
v-else-if="field.type === 'number'"
v-model="form[field.prop]"
:min="0"
:precision="6"
:controls="false"
:disabled="!editable || form.currency === 'RMB'"
@change="recalculateLocalAmount"
/>
<el-input
v-else-if="field.type === 'textarea'"
v-model="form[field.prop]"
type="textarea"
:rows="2"
:maxlength="field.maxlength"
show-word-limit
:disabled="!editable"
placeholder="请输入"
/>
<span v-else-if="field.prop === 'settlementType'" class="form-readonly">
{{ settlementTypeName }}
</span>
<span v-else-if="field.money" class="form-readonly">
{{ formatMoney(form[field.prop], moneyCurrency(field.prop)) }}
</span>
<span v-else class="form-readonly">{{ displayValue(form[field.prop]) }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-input-number
v-else-if="field.type === 'number'"
v-model="form[field.prop]"
:min="0"
:precision="6"
:controls="false"
:disabled="!editable || form.currency === 'RMB'"
@change="recalculateLocalAmount"
/>
<el-input
v-else-if="field.type === 'textarea'"
v-model="form[field.prop]"
type="textarea"
:rows="2"
:maxlength="field.maxlength"
show-word-limit
:disabled="!editable"
placeholder="请输入"
/>
<span v-else-if="field.prop === 'settlementType'" class="form-readonly">
{{ settlementTypeName }}
</span>
<span v-else-if="field.money" class="form-readonly">
{{ formatMoney(form[field.prop], moneyCurrency(field.prop)) }}
</span>
<span v-else class="form-readonly">{{ displayValue(form[field.prop]) }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
</section-card>
<section-card title="结算合计">
@@ -130,7 +126,6 @@
<el-input-number
v-else-if="editable && column.prop === 'adjustAmount'"
v-model="row.adjustAmount"
:min="row.manualFlag === 1 ? 0 : undefined"
:precision="2"
:controls="false"
@change="recalculateSummaryRow(row)"
@@ -231,7 +226,7 @@
<el-table :data="filteredDetails" border class="pre-settlement-editor__detail-table">
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table-column
v-for="column in detailColumns"
v-for="column in visibleDetailColumns"
:key="column.prop"
:label="column.label"
:prop="column.prop"
@@ -253,6 +248,12 @@
<span v-else-if="column.prop === 'transportQuantityText'">
{{ formatQuantity(row) }}
</span>
<span v-else-if="pageMode && column.prop === 'mileage'">
{{ formatDetailMileage(row[column.prop]) }}
</span>
<span v-else-if="pageMode && column.prop === 'transportType'">
{{ transportTypeName(row[column.prop]) }}
</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
@@ -265,7 +266,10 @@
>
<template #default="{ row }">
{{
formatMoney(parseFeeItems(row.feeItemsJson)[feeItem], row.currency || form.currency)
formatMoney(
parseFeeItems(row.feeItemsJson)[feeItem],
row.currency || form.currency
)
}}
</template>
</el-table-column>
@@ -301,19 +305,11 @@
button-text="上传附件"
@change="handleAttachmentChange"
/>
<el-button
type="primary"
:disabled="!attachments.length"
@click="downloadAttachments"
>
<el-button type="primary" :disabled="!attachments.length" @click="downloadAttachments">
批量下载
</el-button>
</div>
<el-table
:data="attachments"
border
@selection-change="handleAttachmentSelectionChange"
>
<el-table :data="attachments" border @selection-change="handleAttachmentSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="260" show-overflow-tooltip>
@@ -327,7 +323,13 @@
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" sortable />
<el-table-column
prop="uploadTime"
label="上传时间"
width="170"
align="center"
sortable
/>
<el-table-column label="操作" :width="editable ? 150 : 80" fixed="right" align="center">
<template #default="{ row, $index }">
<div class="pre-settlement-editor__links">
@@ -388,7 +390,7 @@
</section-card>
</div>
<template #footer>
<template v-if="!pageMode" #footer>
<el-button @click="visible = false">取消</el-button>
<el-button v-if="editable" :loading="saving" @click="saveDraft(false)">保存</el-button>
<el-button
@@ -400,7 +402,19 @@
提交
</el-button>
</template>
</el-dialog>
<div v-if="pageMode" class="pre-settlement-editor__page-actions">
<el-button @click="visible = false">取消</el-button>
<el-button v-if="editable" :loading="saving" @click="saveDraft(false)">保存</el-button>
<el-button
v-if="editable && hasPermission('pre_settlement_submit')"
type="primary"
:loading="submitting"
@click="saveDraft(true)"
>
提交
</el-button>
</div>
</component>
<el-dialog
v-model="candidateDialog.visible"
@@ -558,17 +572,6 @@
/>
</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"
:disabled="adjustDialog.readonly"
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="200" align="center">
<template #default="{ row }">
<el-input v-model="row.remark" maxlength="200" :disabled="adjustDialog.readonly" />
@@ -644,6 +647,14 @@ export default {
type: Boolean,
default: false,
},
pageMode: {
type: Boolean,
default: false,
},
initialData: {
type: Object,
default: null,
},
},
emits: ['update:modelValue', 'success'],
data() {
@@ -663,6 +674,7 @@ export default {
contractOptions: [],
feeOptions: [],
feeCategoryOptions: [],
transportTypeOptions: [],
summaryFees: [],
details: [],
detailCollapsed: false,
@@ -735,6 +747,25 @@ export default {
this.$emit('update:modelValue', value);
},
},
editorContainer() {
return this.pageMode ? 'div' : 'el-dialog';
},
editorContainerProps() {
if (this.pageMode) {
return {
class: ['pre-settlement-editor', 'pre-settlement-editor--page'],
};
}
return {
modelValue: this.visible,
title: this.dialogTitle,
width: '96%',
top: '2vh',
appendToBody: true,
destroyOnClose: true,
class: 'pre-settlement-editor',
};
},
editable() {
return !this.readonly && ['draft', 'returned'].includes(this.form.approvalStatus);
},
@@ -755,7 +786,28 @@ export default {
return this.form.settlementType === 'receivable' ? '应收' : '应付';
},
summaryTotal() {
return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0);
if (this.summaryFees.length) {
return this.summaryFees.reduce(
(total, row) => total + Number(row.settlementAmount || 0),
0
);
}
return this.details.reduce(
(total, row) =>
total +
Number(
row.settlementAmountTax ??
row.totalAmount ??
row.afterAmount ??
row.settlementAmount ??
0
),
0
);
},
visibleDetailColumns() {
if (!this.pageMode) return this.detailColumns;
return this.detailColumns.filter(column => column.prop !== 'settlementAmountNoTax');
},
detailFeeItemNames() {
const names = new Set();
@@ -783,8 +835,11 @@ export default {
},
},
watch: {
modelValue(value) {
if (value) this.initialize();
modelValue: {
immediate: true,
handler(value) {
if (value) this.initialize();
},
},
summaryTotal(value) {
this.form.settlementAmount = Number(value || 0).toFixed(2);
@@ -800,7 +855,53 @@ export default {
await this.loadContractOptions('');
await this.loadFeeOptions();
await this.loadFeeCategoryOptions();
if (this.pageMode) await this.loadTransportTypeOptions();
if (this.recordId) await this.loadDetail();
else if (this.initialData) this.applyInitialData();
},
applyInitialData() {
const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : [];
if (!rows.length) return;
const first = rows[0];
const settlementType = this.initialData.settlementType || first.settlementType || 'payable';
const contractId = first.contractId || '';
if (!this.contractOptions.some(item => String(item.id) === String(contractId))) {
this.contractOptions.push({
id: contractId,
contractNo: first.contractNo,
contractName: first.contractName,
projectId: first.projectId,
projectName: first.projectName,
deptId: first.deptId,
deptName: first.deptName,
settlementType,
partyA: settlementType === 'receivable' ? first.payeeName : first.payerName,
partyB: settlementType === 'receivable' ? first.payerName : first.payeeName,
});
}
this.handleContractChange(contractId);
Object.assign(this.form, {
contractId,
contractNo: first.contractNo || this.form.contractNo,
contractName: first.contractName || this.form.contractName,
projectId: first.projectId || this.form.projectId,
projectName: first.projectName || this.form.projectName,
deptId: first.deptId || this.form.deptId,
deptName: first.deptName || this.form.deptName,
payerName: first.payerName || this.form.payerName,
payeeName: first.payeeName || this.form.payeeName,
settlementType,
currency: first.currency || 'RMB',
localCurrency: first.localCurrency || 'RMB',
});
this.details = rows.map(row => ({
...row,
sourceDetailId: row.sourceDetailId || row.id,
settlementAmountTax:
row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount,
feeItemsJson: row.feeItemsJson || JSON.stringify(row.feeItems || {}),
}));
this.buildSummaryFeesFromDetails();
},
resetEditor() {
this.form = emptyPreSettlementForm();
@@ -869,6 +970,10 @@ export default {
const { data } = await getDictionary({ code: 'fee_category' });
this.feeCategoryOptions = data?.data || [];
},
async loadTransportTypeOptions() {
const { data } = await getDictionary({ code: 'transport_type' });
this.transportTypeOptions = data?.data || [];
},
handleContractChange(id) {
const contract = this.contractOptions.find(item => String(item.id) === String(id));
if (!contract) {
@@ -914,12 +1019,12 @@ export default {
await submit({ id: this.form.id });
this.$message.success('审批流程已发起');
this.visible = false;
this.$emit('success');
this.$emit('success', this.form.id);
return;
}
this.$message.success('保存成功');
await this.loadDetail();
this.$emit('success');
this.$emit('success', this.form.id);
} finally {
this[stateKey] = false;
}
@@ -968,6 +1073,17 @@ export default {
);
return option?.dictValue || value;
},
transportTypeName(value) {
if (value === undefined || value === null || value === '') return '';
const option = this.transportTypeOptions.find(
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
);
return option?.dictValue || value;
},
formatDetailMileage(value) {
if (value === undefined || value === null || value === '' || Number(value) === -1) return '';
return value;
},
removeSummaryFee(index) {
this.summaryFees.splice(index, 1);
},
@@ -979,6 +1095,75 @@ export default {
this.$message.warning('结算金额不能小于0');
}
},
buildSummaryFeesFromDetails() {
const summaryMap = new Map();
const defaultFreightItem =
this.feeOptions
.flatMap(item => item.feeItems || [])
.find(name => this.isFreightFeeItem(name)) || '运输费';
const appendSummary = (feeItem, value, fallbackFeeType = '') => {
const amount = Number(value || 0);
if (!feeItem || !Number.isFinite(amount) || Math.abs(amount) < 0.005) return;
const feeOption = this.feeOptions.find(item =>
(item.feeItems || []).some(name => String(name) === String(feeItem))
);
const feeType = feeOption?.feeType || fallbackFeeType || '';
const key = `${feeType}\u0000${feeItem}`;
const current = summaryMap.get(key) || {
id: '',
feeType,
feeItem,
originalAmount: 0,
adjustAmount: 0,
settlementAmount: 0,
remark: '',
manualFlag: 0,
};
current.originalAmount = Number((current.originalAmount + amount).toFixed(2));
current.settlementAmount = current.originalAmount;
summaryMap.set(key, current);
};
this.details.forEach(row => {
const feeItems = this.parseFeeItems(row.feeItemsJson || row.feeItems);
const feeItemEntries = Object.entries(feeItems);
feeItemEntries.forEach(([feeItem, amount]) =>
appendSummary(feeItem, amount, row.feeType)
);
let knownAmount = feeItemEntries.reduce(
(total, [, amount]) => total + Number(amount || 0),
0
);
if (!feeItemEntries.some(([feeItem]) => this.isFreightFeeItem(feeItem))) {
const freightAmount = Number(row.freightAmount || 0);
appendSummary(defaultFreightItem, freightAmount, row.feeType);
knownAmount += freightAmount;
}
if (!feeItemEntries.some(([feeItem]) => !this.isFreightFeeItem(feeItem))) {
const otherFeeAmount = Number(row.otherFeeAmount || 0);
appendSummary('其他费用', otherFeeAmount, row.feeType);
knownAmount += otherFeeAmount;
}
const totalAmount = Number(
row.settlementAmountTax ??
row.totalAmount ??
row.afterAmount ??
row.settlementAmount ??
0
);
const residualAmount = Number((totalAmount - knownAmount).toFixed(2));
if (residualAmount > 0) appendSummary('其他费用', residualAmount, row.feeType);
});
const manualFees = this.summaryFees.filter(row => row.manualFlag === 1);
this.summaryFees = [...summaryMap.values(), ...manualFees];
this.form.settlementAmount = this.summaryFees
.reduce((total, row) => total + Number(row.settlementAmount || 0), 0)
.toFixed(2);
this.recalculateLocalAmount();
},
recalculateLocalAmount() {
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0);
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
@@ -1054,12 +1239,13 @@ export default {
await this.$confirm('确认将该明细踢出预结算单?', '提示', { type: 'warning' });
if (!this.form.id || !row.id || row.id === row.sourceDetailId) {
this.details = this.details.filter(item => item !== row);
this.buildSummaryFeesFromDetails();
return;
}
await removeDetail(this.form.id, row.id);
this.$message.success('已移除');
await this.loadDetail();
this.$emit('success');
this.$emit('success', this.form.id);
},
handleDetailQuery() {
this.appliedDetailQuery = { ...this.detailQuery };
@@ -1138,7 +1324,7 @@ export default {
this.adjustDialog.visible = false;
this.$message.success('结算明细调整成功');
await this.loadDetail();
this.$emit('success');
this.$emit('success', this.form.id);
} finally {
this.adjustDialog.saving = false;
}
@@ -1313,6 +1499,19 @@ export default {
overflow-y: auto;
}
&--page &__content {
max-height: none;
overflow: visible;
}
&__page-actions {
display: flex;
justify-content: flex-end;
padding: 16px 0 4px;
border-top: 1px solid #eff1f7;
gap: 8px;
}
&__form {
:deep(.el-select),
:deep(.el-date-editor),
@@ -0,0 +1,88 @@
<template>
<basic-container class="formal-settlement-form-page">
<div class="formal-settlement-form-page__title">{{ pageTitle }}</div>
<formal-settlement-editor
v-model="editorVisible"
page-mode
:record-id="recordId"
:readonly="readonly"
:initial-data="transferPayload"
/>
</basic-container>
</template>
<script>
import FormalSettlementEditor from './components/formal-settlement-editor.vue';
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
export default {
name: 'FormalSettlementForm',
components: { FormalSettlementEditor },
data() {
return {
editorVisible: true,
transferPayload: null,
};
},
computed: {
recordId() {
return this.$route.query.id || '';
},
readonly() {
return this.$route.query.mode === 'view';
},
pageTitle() {
return this.readonly ? '查看正式结算' : this.recordId ? '编辑正式结算' : '新增正式结算';
},
},
watch: {
editorVisible(value) {
if (!value) this.goBack();
},
},
created() {
this.transferPayload = readSettlementTransfer(this.$route.query.transferToken);
this.syncTagTitle();
},
methods: {
goBack() {
removeSettlementTransfer(this.$route.query.transferToken);
this.$router.push('/settlement/formal-settlement');
},
syncTagTitle() {
this.$nextTick(() => {
this.$store.commit('SET_TAG', {
fullPath: this.$route.fullPath,
name: this.pageTitle,
});
this.$router.$avueRouter.setTitle(this.pageTitle);
});
},
},
};
</script>
<style lang="scss" scoped>
.formal-settlement-form-page {
&__title {
display: flex;
align-items: center;
min-height: 24px;
margin-bottom: 16px;
font-size: 18px;
font-weight: 600;
&::before {
width: 4px;
height: 20px;
margin-right: 8px;
background: #409eff;
content: '';
}
}
}
:deep(.formal-settlement-form-page.basic-container .basic-container__card > .el-card__body) {
padding: 0;
}
</style>
+13 -3
View File
@@ -41,7 +41,11 @@
</section>
<section class="formal-page__table-panel">
<el-tabs type="border-card" v-model="activeSettlementType" @tab-change="handleSettlementTypeChange">
<el-tabs
type="border-card"
v-model="activeSettlementType"
@tab-change="handleSettlementTypeChange"
>
<el-tab-pane label="应付" name="payable" />
<el-tab-pane label="应收" name="receivable" />
</el-tabs>
@@ -336,10 +340,16 @@ export default {
this.loadTable();
},
openCreate() {
this.editor = { visible: true, id: null, readonly: false };
this.$router.push({
path: '/settlement/formal-settlement/form',
query: { mode: 'add', name: '新增正式结算' },
});
},
openEdit(row) {
this.editor = { visible: true, id: row.id, readonly: false };
this.$router.push({
path: '/settlement/formal-settlement/form',
query: { mode: 'edit', id: row.id, name: '编辑正式结算' },
});
},
openView(row) {
this.editor = { visible: true, id: row.id, readonly: true };
@@ -0,0 +1,90 @@
<template>
<basic-container class="pre-settlement-form-page">
<div class="pre-settlement-form-page__title">{{ pageTitle }}</div>
<pre-settlement-editor
v-model="editorVisible"
page-mode
:record-id="recordId"
:initial-data="transferPayload"
@success="handleSuccess"
/>
</basic-container>
</template>
<script>
import PreSettlementEditor from './components/pre-settlement-editor.vue';
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
export default {
name: 'PreSettlementForm',
components: { PreSettlementEditor },
data() {
return {
editorVisible: true,
savedRecordId: '',
transferPayload: null,
};
},
computed: {
recordId() {
return this.$route.query.id || '';
},
pageTitle() {
return this.recordId || this.savedRecordId ? '编辑预结算' : '新增预结算';
},
},
watch: {
editorVisible(value) {
if (!value) this.goBack();
},
},
created() {
this.transferPayload = readSettlementTransfer(this.$route.query.transferToken);
this.syncTagTitle();
},
methods: {
handleSuccess(recordId) {
this.savedRecordId = recordId || this.savedRecordId;
this.syncTagTitle();
},
goBack() {
removeSettlementTransfer(this.$route.query.transferToken);
this.$router.push('/settlement/pre-settlement');
},
syncTagTitle() {
this.$nextTick(() => {
this.$store.commit('SET_TAG', {
fullPath: this.$route.fullPath,
name: this.pageTitle,
});
this.$router.$avueRouter.setTitle(this.pageTitle);
});
},
},
};
</script>
<style lang="scss" scoped>
.pre-settlement-form-page {
&__title {
display: flex;
align-items: center;
min-height: 24px;
margin-bottom: 16px;
font-size: 18px;
font-weight: 600;
&::before {
width: 4px;
height: 20px;
margin-right: 8px;
background: #409eff;
content: '';
}
}
}
:deep(.pre-settlement-form-page.basic-container .basic-container__card > .el-card__body) {
padding: 0;
}
</style>
+8 -2
View File
@@ -473,10 +473,16 @@ export default {
this.selection = rows;
},
openCreate() {
this.editor = { visible: true, id: '', readonly: false };
this.$router.push({
path: '/settlement/pre-settlement/form',
query: { mode: 'add', name: '新增预结算' },
});
},
openEdit(row) {
this.editor = { visible: true, id: row.id, readonly: false };
this.$router.push({
path: '/settlement/pre-settlement/form',
query: { mode: 'edit', id: row.id, name: '编辑预结算' },
});
},
openView(row) {
this.editor = { visible: true, id: row.id, readonly: true };
@@ -79,10 +79,18 @@
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="{ row }">
<div class="settlement-detail-page__actions">
<el-link v-if="row.settlementStatus === 'pending'" type="primary" @click="openAdjustDialog(row)">
<el-link
v-if="row.settlementStatus === 'pending'"
type="primary"
@click="openAdjustDialog(row)"
>
调整
</el-link>
<el-link v-if="row.settlementStatus === 'pending'" type="danger" @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>
@@ -112,12 +120,7 @@
<header class="settlement-detail-page__detail-panel-header">
<h3>单据详情{{ detailDialog.title }}</h3>
<el-tooltip content="关闭" placement="top">
<el-button
:icon="Close"
text
aria-label="关闭单据详情"
@click="closeDetailPanel"
/>
<el-button :icon="Close" text aria-label="关闭单据详情" @click="closeDetailPanel" />
</el-tooltip>
</header>
<div class="settlement-detail-page__detail-panel-body">
@@ -180,12 +183,7 @@
<header class="settlement-detail-page__detail-panel-header">
<h3>调整费用{{ adjustDialog.row?.documentNo || '-' }}</h3>
<el-tooltip content="关闭" placement="top">
<el-button
:icon="Close"
text
aria-label="关闭调整费用"
@click="closeAdjustPanel"
/>
<el-button :icon="Close" text aria-label="关闭调整费用" @click="closeAdjustPanel" />
</el-tooltip>
</header>
<div
@@ -346,7 +344,6 @@
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadTransferCandidates">查询</el-button>
<el-button type="primary" @click="confirmTransferSelection">确认选择</el-button>
</el-form-item>
</el-form>
</div>
@@ -477,11 +474,7 @@
</div>
<template #footer>
<el-button @click="generateDialog.visible = false">取消</el-button>
<el-button
type="primary"
:loading="previewDialog.loading"
@click="openGeneratePreview"
>
<el-button type="primary" :loading="previewDialog.loading" @click="openGeneratePreview">
生成费用
</el-button>
</template>
@@ -513,7 +506,7 @@
</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" @click="backToGenerateDialog">上一步</el-button>
<el-button type="primary" :loading="previewDialog.submitting" @click="submitGenerateFee">
提交
</el-button>
@@ -537,9 +530,11 @@ import {
} from '@/option/settlement/receivable-payable-detail';
import * as api from '@/api/settlement/receivable-payable-detail';
import { getList as getContractList } from '@/api/business/contract-manage';
import { getContractOptions as getSettlementContractOptions } from '@/api/settlement/preSettlement';
import { exportBlob } from '@/api/common';
import { getDictionary } from '@/api/system/dictbiz';
import { downloadXls } from '@/utils/util';
import { createSettlementTransfer } from '@/utils/settlement-transfer';
export default {
props: {
@@ -567,7 +562,13 @@ export default {
rows: [],
selection: [],
page: { current: 1, size: 10, total: 0 },
detailDialog: { visible: false, title: '费用明细', activeTab: 'fee', row: null, loading: false },
detailDialog: {
visible: false,
title: '费用明细',
activeTab: 'fee',
row: null,
loading: false,
},
feeRows: [],
dynamicFeeColumns: [],
adjustDialog: { visible: false, loading: false, submitting: false, row: null },
@@ -664,7 +665,10 @@ export default {
if (column.prop === 'transportType') return this.transportTypeLabel(row[column.prop]);
if (column.prop === 'transportQuantity') return this.fixedTwoDecimals(row[column.prop]);
if (column.dynamic) {
return this.money(this.normalizeFeeItems(row.feeItems)[column.feeItemName], row.currency || 'RMB');
return this.money(
this.normalizeFeeItems(row.feeItems)[column.feeItemName],
row.currency || 'RMB'
);
}
return this.formatDetailCell(row, column.prop);
},
@@ -673,7 +677,12 @@ export default {
this.tableFeeItemNames = [];
try {
const params = this.buildRequestParams(
this.normalizeQuery(this.query, 'generateDateRange', 'generateStartDate', 'generateEndDate')
this.normalizeQuery(
this.query,
'generateDateRange',
'generateStartDate',
'generateEndDate'
)
);
const res = await api.getList(this.page.current, this.page.size, params);
const data = this.unwrapPage(res);
@@ -685,8 +694,7 @@ export default {
}
},
async loadContracts() {
const contractCategory =
this.settlementType === 'payable' ? '承运商合同' : '客户合同';
const contractCategory = this.settlementType === 'payable' ? '承运商合同' : '客户合同';
this.contractLoading = true;
try {
const res = await getContractList(1, 999, {
@@ -746,7 +754,13 @@ export default {
},
async openDetailDialog(row) {
this.closeAdjustPanel();
this.detailDialog = { ...this.detailDialog, visible: true, row, title: row.documentNo, activeTab: 'fee' };
this.detailDialog = {
...this.detailDialog,
visible: true,
row,
title: row.documentNo,
activeTab: 'fee',
};
await this.loadFeeDetail();
},
async openAdjustDialog(row) {
@@ -778,13 +792,9 @@ export default {
originalAmount: Number(item.originalAmount || 0),
feeItems,
manualFee: item.billingFactor === '手工调整',
feeItemName:
item.billingFactor === '手工调整' ? Object.keys(feeItems)[0] || '' : '',
feeItemName: item.billingFactor === '手工调整' ? Object.keys(feeItems)[0] || '' : '',
feeType: item.billingType === '手工扣费' ? 'deduct' : 'charge',
amount:
item.billingFactor === '手工调整'
? Math.abs(Number(item.afterAmount || 0))
: 0,
amount: item.billingFactor === '手工调整' ? Math.abs(Number(item.afterAmount || 0)) : 0,
};
if (adjusted.manualFee) this.recalculateManualAdjustRow(adjusted);
else this.recalculateAdjustRow(adjusted);
@@ -870,7 +880,9 @@ export default {
0
);
const hasFreightItem = Object.keys(row.feeItems).some(this.isFreightFeeItem);
row.afterAmount = Number((hasFreightItem ? feeItemTotal : Number(row.freightAmount || 0) + feeItemTotal).toFixed(2));
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) {
@@ -882,7 +894,8 @@ export default {
return;
}
const invalidManualRow = this.adjustRows.find(
row => row.manualFee && (!String(row.feeItemName || '').trim() || Number(row.amount || 0) <= 0)
row =>
row.manualFee && (!String(row.feeItemName || '').trim() || Number(row.amount || 0) <= 0)
);
if (invalidManualRow) {
this.$message.warning('请完整填写手工费用项目和金额');
@@ -1050,7 +1063,9 @@ export default {
return value !== null && value !== undefined && String(value).trim() !== '';
};
const normalizeSettlementType = value => {
const normalized = String(value || '').trim().toLowerCase();
const normalized = String(value || '')
.trim()
.toLowerCase();
if (normalized === '应收') return 'receivable';
if (normalized === '应付') return 'payable';
return normalized;
@@ -1061,12 +1076,13 @@ export default {
const expectedSettlementType = normalizeSettlementType(this.settlementType);
const matchesSettlementType =
!expectedSettlementType || rowSettlementType === expectedSettlementType;
const status = String(row.settlementStatus || '').trim().toLowerCase();
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);
) || ['closed', 'close', '已关闭'].includes(status);
const hasPreSettlement = [
row.preSettlementId,
row.preSettlementIds,
@@ -1087,13 +1103,6 @@ export default {
!hasFormalSettlement
);
},
confirmTransferSelection() {
if (!this.transferSelection.length) {
this.$message.warning('请选择需要转结算的明细');
return;
}
this.$message.success(`已选择${this.transferSelection.length}条明细`);
},
transferSelectionChange(selection) {
this.transferSelection = selection;
},
@@ -1102,20 +1111,57 @@ export default {
this.$message.warning('请选择需要转结算的明细');
return;
}
if (this.transferSelection.some(item => !item.contractId && !item.contractNo)) {
this.$message.warning('所选明细缺少合同信息,无法转结算');
return;
}
const contractKeys = new Set(
this.transferSelection.map(item =>
item.contractNo ? `no:${item.contractNo}` : `id:${item.contractId}`
)
);
if (contractKeys.size > 1) {
this.$message.warning('请选择同一合同下的明细进行转结算');
return;
}
this.transferDialog.submitting = true;
try {
await api.transferSettlement({
settlementType: this.settlementType || undefined,
settlementBillType: this.transferForm.settlementBillType,
ids: this.transferSelection.map(item => item.id),
const transferRows = await this.resolveTransferContractIds(this.transferSelection);
if (!transferRows) return;
const settlementBillType = this.transferForm.settlementBillType;
const transferToken = createSettlementTransfer({
settlementBillType,
settlementType: this.settlementType || transferRows[0]?.settlementType,
rows: transferRows,
});
this.$message.success('转结算成功');
const isPreSettlement = settlementBillType === 'pre';
this.transferDialog.visible = false;
this.loadTable();
await this.$router.push({
path: isPreSettlement
? '/settlement/pre-settlement/form'
: '/settlement/formal-settlement/form',
query: {
mode: 'add',
transferToken,
name: isPreSettlement ? '新增预结算' : '新增正式结算',
},
});
} finally {
this.transferDialog.submitting = false;
}
},
async resolveTransferContractIds(rows) {
if (rows.every(item => item.contractId)) return rows;
const contractNo = rows.find(item => item.contractNo)?.contractNo;
const { data } = await getSettlementContractOptions(contractNo);
const contracts = data?.data || [];
const contract = contracts.find(item => String(item.contractNo) === String(contractNo));
if (!contract?.id) {
this.$message.warning('未找到所选明细对应的有效合同,无法转结算');
return null;
}
return rows.map(item => ({ ...item, contractId: item.contractId || contract.id }));
},
openGenerateDialog() {
this.generateDialog.visible = true;
this.generateQuery = {};
@@ -1127,8 +1173,9 @@ export default {
this.loadContracts();
},
handleGenerateContractChange(contractId) {
this.syncBillingPlanOptions(contractId);
this.generateQuery.billingPlanId = '';
const options = this.syncBillingPlanOptions(contractId);
const selected = options.find(item => item.defaultPlan) || options[0];
this.generateQuery.billingPlanId = selected?.id || '';
},
async loadGenerateWaybills() {
if (!this.generateQuery.contractId || !this.generateQuery.billingPlanId) {
@@ -1138,9 +1185,18 @@ export default {
this.generateDialog.loading = true;
try {
const params = this.buildRequestParams(
this.normalizeQuery(this.generateQuery, 'finishDateRange', 'finishStartDate', 'finishEndDate')
this.normalizeQuery(
this.generateQuery,
'finishDateRange',
'finishStartDate',
'finishEndDate'
)
);
const res = await api.getGenerateWaybills(
this.generatePage.current,
this.generatePage.size,
params
);
const res = await api.getGenerateWaybills(this.generatePage.current, this.generatePage.size, params);
const data = this.unwrapPage(res);
this.generateRows = data.records || [];
this.generatePage.total = data.total || 0;
@@ -1188,12 +1244,49 @@ export default {
minWidth: 130,
align: 'right',
}));
const selectedWaybillsById = new Map();
const selectedWaybillsByNo = new Map();
this.generateSelection.forEach(item => {
[item.id, item.waybillId, item.sourceWaybillId].forEach(id => {
if (id !== null && id !== undefined && id !== '') {
selectedWaybillsById.set(String(id), item);
}
});
if (item.waybillNo) selectedWaybillsByNo.set(String(item.waybillNo), item);
});
this.previewRows = (data.records || []).map(row => {
const dynamic = {};
(data.feeItemNames || []).forEach((name, index) => {
dynamic[`feeItem${index}`] = row.feeItems?.[name] || '';
});
return { ...row, ...dynamic };
const sourceWaybillId =
row.waybillId || row.sourceWaybillId || row.sourceId || row.businessId || row.id;
const sourceWaybill =
selectedWaybillsById.get(String(sourceWaybillId || '')) ||
selectedWaybillsByNo.get(String(row.waybillNo || row.waybillNumber || ''));
return {
...row,
customerName:
row.customerName ||
row.customerUnitName ||
row.clientName ||
sourceWaybill?.customerName ||
'',
waybillNo:
row.waybillNo ||
row.waybillNumber ||
row.sourceWaybillNo ||
sourceWaybill?.waybillNo ||
'',
vehicleNo:
row.vehicleNo ||
row.plateNo ||
row.carNo ||
row.vehicleNumber ||
sourceWaybill?.vehicleNo ||
'',
...dynamic,
};
});
this.previewPage.total = data.total || 0;
return true;
@@ -1207,6 +1300,12 @@ export default {
this.previewPage.current = 1;
this.loadGeneratePreview();
},
backToGenerateDialog() {
this.previewDialog.visible = false;
this.$nextTick(() => {
this.generateDialog.visible = true;
});
},
async submitGenerateFee() {
this.previewDialog.submitting = true;
try {
@@ -1227,7 +1326,12 @@ export default {
exportBlob(
'/blade-transport/receivable-payable-detail/export-receivable-payable-detail',
this.buildRequestParams(
this.normalizeQuery(this.query, 'generateDateRange', 'generateStartDate', 'generateEndDate')
this.normalizeQuery(
this.query,
'generateDateRange',
'generateStartDate',
'generateEndDate'
)
),
{ feedback: true }
).then(res => {
@@ -1254,7 +1358,9 @@ export default {
`plan-${index}`,
name: item.name || item.planName || item.billingPlanName || `计费方案${index + 1}`,
defaultPlan:
item.defaultPlan === true || String(item.defaultPlan).toLowerCase() === 'true',
item.defaultPlan === true ||
item.defaultPlan === 1 ||
['true', '1'].includes(String(item.defaultPlan).toLowerCase()),
}));
return this.billingPlanOptions;
},