Compare commits
6 Commits
21c4168867
...
9405c51de8
| Author | SHA1 | Date | |
|---|---|---|---|
| 9405c51de8 | |||
| e5992a0c63 | |||
| 1ef050dda6 | |||
| e1bade226f | |||
| 44e31fbd22 | |||
| 5fcb82c0c0 |
@@ -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`,
|
||||
|
||||
@@ -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 } });
|
||||
@@ -28,5 +30,10 @@ export const getProjectContracts = projectId =>
|
||||
request({
|
||||
url: '/blade-transport/contract-manage/list',
|
||||
method: 'get',
|
||||
params: { current: 1, size: 9999, projectId },
|
||||
params: {
|
||||
current: 1,
|
||||
size: 9999,
|
||||
projectId,
|
||||
contractCategory: '客户合同',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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' },
|
||||
];
|
||||
@@ -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' },
|
||||
];
|
||||
@@ -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' },
|
||||
];
|
||||
@@ -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' },
|
||||
];
|
||||
@@ -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' },
|
||||
];
|
||||
@@ -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' },
|
||||
];
|
||||
@@ -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' },
|
||||
];
|
||||
@@ -11,8 +11,10 @@ export const getCandidates = (current, size, params) =>
|
||||
method: 'get',
|
||||
params: { current, size, ...params },
|
||||
});
|
||||
export const getContractOptions = keyword =>
|
||||
request({ url: `${baseUrl}/contract-options`, method: 'get', params: { keyword } });
|
||||
export const getContractOptions = (keyword, projectId) =>
|
||||
request({ url: `${baseUrl}/contract-options`, method: 'get', params: { keyword, projectId } });
|
||||
export const getNextNo = () => request({ url: `${baseUrl}/next-no`, method: 'get' });
|
||||
export const getFeeOptions = () => request({ url: `${baseUrl}/fee-options`, method: 'get' });
|
||||
export const getCandidateDetails = (current, size, params) =>
|
||||
request({
|
||||
url: `${baseUrl}/candidate-details`,
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="地图选择地址"
|
||||
width="920px"
|
||||
append-to-body
|
||||
@opened="initMap"
|
||||
>
|
||||
<div class="address-map-picker__toolbar">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
clearable
|
||||
placeholder="输入地址关键词"
|
||||
@keyup.enter="searchKeyword"
|
||||
/>
|
||||
<el-button type="primary" :loading="searching" @click="searchKeyword">搜索</el-button>
|
||||
</div>
|
||||
<div ref="map" class="address-map-picker__map"></div>
|
||||
<div class="address-map-picker__info">{{ status }}</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected.longitude || resolving" @click="confirm">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
let amapLoader;
|
||||
|
||||
export default {
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'confirm'],
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
keyword: '',
|
||||
status: '可搜索地址或点击地图选点',
|
||||
searching: false,
|
||||
resolving: false,
|
||||
selected: {},
|
||||
amap: null,
|
||||
marker: null,
|
||||
geocoder: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
this.visible = value;
|
||||
if (value) {
|
||||
this.keyword = this.address || '';
|
||||
this.selected = {};
|
||||
this.status = '可搜索地址或点击地图选点';
|
||||
}
|
||||
},
|
||||
},
|
||||
visible(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.amap) {
|
||||
this.amap.destroy();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadAmap() {
|
||||
if (window.AMap && window.AMap.Map) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!amapLoader) {
|
||||
amapLoader = new Promise((resolve, reject) => {
|
||||
window._AMapSecurityConfig = { securityJsCode: AMAP_SECURITY_CODE };
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`;
|
||||
script.async = true;
|
||||
script.onload = resolve;
|
||||
script.onerror = () => reject(new Error('高德地图组件加载失败'));
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
}
|
||||
return amapLoader;
|
||||
},
|
||||
initMap() {
|
||||
this.loadAmap()
|
||||
.then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (!this.amap) {
|
||||
this.amap = new window.AMap.Map(this.$refs.map, {
|
||||
center: [116.40769, 39.89945],
|
||||
zoom: 11,
|
||||
});
|
||||
this.amap.on('click', event => this.pickPoint(event.lnglat));
|
||||
} else {
|
||||
this.amap.resize();
|
||||
this.clearMarker();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('高德地图组件加载失败,请稍后重试');
|
||||
this.visible = false;
|
||||
});
|
||||
},
|
||||
ensureGeocoder() {
|
||||
if (this.geocoder) return Promise.resolve(this.geocoder);
|
||||
return new Promise((resolve, reject) => {
|
||||
window.AMap.plugin(['AMap.Geocoder'], () => {
|
||||
try {
|
||||
this.geocoder = new window.AMap.Geocoder();
|
||||
resolve(this.geocoder);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
runGeocode(action, input) {
|
||||
return this.ensureGeocoder().then(
|
||||
geocoder =>
|
||||
new Promise((resolve, reject) => {
|
||||
const done = (status, result) => {
|
||||
if (status === 'complete' && result) {
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(new Error('高德地图请求失败'));
|
||||
}
|
||||
};
|
||||
if (action === 'location') {
|
||||
geocoder.getLocation(input, done);
|
||||
} else {
|
||||
geocoder.getAddress(input, done);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
searchKeyword() {
|
||||
const keyword = String(this.keyword || '').trim();
|
||||
if (!keyword) {
|
||||
this.$message.warning('请输入地址关键词');
|
||||
return;
|
||||
}
|
||||
this.searching = true;
|
||||
this.loadAmap()
|
||||
.then(() => this.runGeocode('location', keyword))
|
||||
.then(result => {
|
||||
const point = result.geocodes?.[0]?.location || result.location;
|
||||
if (!point) {
|
||||
throw new Error('未找到匹配地址');
|
||||
}
|
||||
this.pickPoint(point, keyword);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
})
|
||||
.finally(() => {
|
||||
this.searching = false;
|
||||
});
|
||||
},
|
||||
pickPoint(point, fallbackAddress = '') {
|
||||
const longitude = typeof point.getLng === 'function' ? point.getLng() : point.lng;
|
||||
const latitude = typeof point.getLat === 'function' ? point.getLat() : point.lat;
|
||||
if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
|
||||
this.$message.warning('选点坐标无效');
|
||||
return;
|
||||
}
|
||||
const lnglat = new window.AMap.LngLat(Number(longitude), Number(latitude));
|
||||
this.renderMarker(lnglat);
|
||||
this.selected = { longitude: Number(longitude), latitude: Number(latitude), address: fallbackAddress };
|
||||
this.status = '正在解析地址...';
|
||||
this.resolving = true;
|
||||
this.runGeocode('address', lnglat)
|
||||
.then(result => {
|
||||
const detailAddress = result.regeocode?.formattedAddress || fallbackAddress;
|
||||
this.selected = { ...this.selected, address: detailAddress };
|
||||
this.keyword = detailAddress || this.keyword;
|
||||
this.status = detailAddress || '已选点,可确认回填';
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = '地址解析失败,请重新选点';
|
||||
this.$message.error('地址解析失败,请重新选点');
|
||||
this.selected = {};
|
||||
this.clearMarker();
|
||||
})
|
||||
.finally(() => {
|
||||
this.resolving = false;
|
||||
});
|
||||
},
|
||||
renderMarker(point) {
|
||||
this.clearMarker();
|
||||
this.marker = new window.AMap.Marker({ position: point });
|
||||
this.marker.setMap(this.amap);
|
||||
this.amap.setCenter(point);
|
||||
},
|
||||
clearMarker() {
|
||||
if (this.marker) {
|
||||
this.marker.setMap(null);
|
||||
this.marker = null;
|
||||
}
|
||||
},
|
||||
confirm() {
|
||||
if (!this.selected.address) {
|
||||
this.$message.warning('请等待地址解析完成后再确认');
|
||||
return;
|
||||
}
|
||||
this.$emit('confirm', this.selected.address);
|
||||
this.visible = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.address-map-picker {
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__map {
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
border: 1px solid #eff1f7;
|
||||
}
|
||||
|
||||
&__info {
|
||||
margin-top: 10px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -202,6 +202,7 @@ export const option = createCrudOption([
|
||||
label: '单价',
|
||||
prop: 'cargoValue',
|
||||
formslot: true,
|
||||
formatter: row => (String(row.cargoValue) === '-1' ? '' : row.cargoValue),
|
||||
minWidth: 120,
|
||||
rules: [
|
||||
{
|
||||
@@ -277,6 +278,9 @@ export const option = createCrudOption([
|
||||
),
|
||||
]);
|
||||
|
||||
// 搜索栏标签统一加宽,给较长的货物类型字段预留足够展示空间。
|
||||
option.searchLabelWidth = 160;
|
||||
|
||||
export const excelOption = {
|
||||
submitBtn: false,
|
||||
emptyBtn: false,
|
||||
|
||||
@@ -34,6 +34,24 @@ export const option = {
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '发货省ID',
|
||||
prop: 'departureProvinceId',
|
||||
hide: true,
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
label: '发货市ID',
|
||||
prop: 'departureCityId',
|
||||
hide: true,
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
label: '发货区ID',
|
||||
prop: 'departureDistrictId',
|
||||
hide: true,
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
label: '发货地址',
|
||||
prop: 'departureAddress',
|
||||
@@ -82,6 +100,24 @@ export const option = {
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '收货省ID',
|
||||
prop: 'arrivalProvinceId',
|
||||
hide: true,
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
label: '收货市ID',
|
||||
prop: 'arrivalCityId',
|
||||
hide: true,
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
label: '收货区ID',
|
||||
prop: 'arrivalDistrictId',
|
||||
hide: true,
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
label: '收货地址',
|
||||
prop: 'arrivalAddress',
|
||||
|
||||
@@ -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: '基本信息',
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
@@ -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 },
|
||||
];
|
||||
@@ -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 },
|
||||
];
|
||||
@@ -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 },
|
||||
];
|
||||
@@ -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 },
|
||||
];
|
||||
@@ -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 },
|
||||
];
|
||||
@@ -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 },
|
||||
];
|
||||
@@ -1,6 +1,6 @@
|
||||
export const formalSettlementFormFields = [
|
||||
{ label: '结算单号', prop: 'formalSettlementNo', readonly: true },
|
||||
{ label: '项目名称', prop: 'projectName', readonly: true },
|
||||
{ label: '项目名称', prop: 'projectId', type: 'project', required: true },
|
||||
{ label: '所属组织', prop: 'deptName', readonly: true },
|
||||
{ label: '合同编号', prop: 'contractNo', readonly: true },
|
||||
{ label: '合同名称', prop: 'contractId', type: 'contract', required: true },
|
||||
@@ -18,6 +18,8 @@ export const formalSettlementFormFields = [
|
||||
export const createFormalSettlementForm = () => ({
|
||||
id: null,
|
||||
formalSettlementNo: '',
|
||||
projectId: null,
|
||||
projectName: '',
|
||||
contractId: null,
|
||||
contractName: '',
|
||||
settlementType: '',
|
||||
|
||||
@@ -52,6 +52,15 @@ export const detailColumns = [
|
||||
{ label: '备注', prop: 'remark', minWidth: 180 },
|
||||
];
|
||||
|
||||
export const summaryColumns = [
|
||||
{ label: '费用类型', prop: 'feeType', minWidth: 150 },
|
||||
{ label: '费用项', prop: 'feeItem', minWidth: 160 },
|
||||
{ label: '原金额', prop: 'originalAmount', minWidth: 130 },
|
||||
{ label: '调整金额', prop: 'adjustAmount', minWidth: 140 },
|
||||
{ label: '结算金额', prop: 'settlementAmount', minWidth: 140 },
|
||||
{ label: '备注', prop: 'remark', minWidth: 200 },
|
||||
];
|
||||
|
||||
export const candidateColumns = [
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', minWidth: 170 },
|
||||
{ label: '项目名称', prop: 'projectName', minWidth: 140 },
|
||||
@@ -73,6 +82,8 @@ export const candidateDetailColumns = [
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 150 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 170 },
|
||||
{ label: '来源', prop: 'sourceType', minWidth: 110 },
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', minWidth: 160 },
|
||||
{ label: '正式结算单号', prop: 'formalSettlementNo', minWidth: 160 },
|
||||
{ label: '运单号', prop: 'waybillNo', minWidth: 150 },
|
||||
{ label: '车号', prop: 'vehicleNo', minWidth: 130 },
|
||||
{ label: '运输类型', prop: 'transportType', minWidth: 120 },
|
||||
|
||||
@@ -50,6 +50,7 @@ export const option = {
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 90,
|
||||
viewBtn: true,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
@@ -128,6 +129,7 @@ export const option = {
|
||||
prop: 'directEconomicLoss',
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
append: '元',
|
||||
minWidth: 140,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
@@ -138,6 +140,7 @@ export const option = {
|
||||
prop: 'insuranceClaimAmount',
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
append: '元',
|
||||
minWidth: 140,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
|
||||
@@ -49,6 +49,7 @@ export const option = {
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 90,
|
||||
viewBtn: true,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
@@ -145,6 +146,7 @@ export const option = {
|
||||
prop: 'fee',
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
append: '元',
|
||||
minWidth: 100,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
|
||||
@@ -30,6 +30,7 @@ export const option = {
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 90,
|
||||
viewBtn: true,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
|
||||
@@ -39,6 +39,7 @@ export const option = {
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 90,
|
||||
viewBtn: true,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
|
||||
@@ -43,6 +43,7 @@ export const option = {
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 90,
|
||||
viewBtn: true,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
|
||||
@@ -35,6 +35,7 @@ export const option = {
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 90,
|
||||
viewBtn: true,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
@@ -70,6 +71,7 @@ export const option = {
|
||||
prop: 'previousMonthMileage',
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
append: 'km',
|
||||
minWidth: 210,
|
||||
slot: true,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
@@ -79,6 +81,7 @@ export const option = {
|
||||
prop: 'currentMonthMileage',
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
append: 'km',
|
||||
minWidth: 210,
|
||||
slot: true,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
@@ -88,6 +91,7 @@ export const option = {
|
||||
prop: 'monthlyMileage',
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
append: 'km',
|
||||
minWidth: 170,
|
||||
slot: true,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
@@ -97,6 +101,7 @@ export const option = {
|
||||
prop: 'totalMileage',
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
append: 'km',
|
||||
minWidth: 170,
|
||||
slot: true,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
|
||||
@@ -36,6 +36,7 @@ export const option = {
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 90,
|
||||
viewBtn: true,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
@@ -123,6 +124,7 @@ export const option = {
|
||||
prop: 'unitPrice',
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
append: '元/升',
|
||||
minWidth: 120,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
|
||||
@@ -47,6 +47,7 @@ export const option = {
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 90,
|
||||
viewBtn: true,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
|
||||
@@ -40,6 +40,7 @@ export const option = {
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 90,
|
||||
viewBtn: true,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
|
||||
@@ -27,6 +27,7 @@ export const option = {
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 90,
|
||||
viewBtn: true,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
|
||||
@@ -50,6 +50,7 @@ export const option = {
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 90,
|
||||
viewBtn: true,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
@@ -127,6 +128,8 @@ export const option = {
|
||||
{
|
||||
label: '地点',
|
||||
prop: 'location',
|
||||
slot: true,
|
||||
formslot: true,
|
||||
minWidth: 180,
|
||||
overHidden: true,
|
||||
span: 12,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
@@ -14,6 +14,7 @@
|
||||
@row-del="rowDel"
|
||||
@search-change="searchChange"
|
||||
@search-reset="searchReset"
|
||||
@selection-change="selectionChange"
|
||||
@current-change="currentChange"
|
||||
@size-change="sizeChange"
|
||||
@refresh-change="refreshChange"
|
||||
@@ -40,6 +41,7 @@
|
||||
v-model="form.feeCategory"
|
||||
class="fee-item-form-control"
|
||||
clearable
|
||||
:disabled="dialogType !== 'add'"
|
||||
filterable
|
||||
placeholder="请选择费用类型"
|
||||
@change="handleFeeCategoryChange"
|
||||
@@ -58,6 +60,7 @@
|
||||
:maxlength="feeItemCodeMaxlength"
|
||||
class="fee-item-form-control"
|
||||
clearable
|
||||
:disabled="dialogType !== 'add'"
|
||||
placeholder="请输入费用项代码"
|
||||
@change="handleFeeItemCodeChange"
|
||||
>
|
||||
@@ -114,12 +117,14 @@ export default {
|
||||
data: [],
|
||||
excelBox: false,
|
||||
excelForm: {},
|
||||
dialogType: 'add',
|
||||
feeCategoryOptions: [],
|
||||
page: {
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
},
|
||||
selectionList: [],
|
||||
option: {
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
@@ -139,7 +144,7 @@ export default {
|
||||
viewBtn: false,
|
||||
delBtn: false,
|
||||
editBtn: false,
|
||||
selection: false,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
menuWidth: 180,
|
||||
column: [
|
||||
@@ -294,6 +299,9 @@ export default {
|
||||
feeItemCodeMaxlength() {
|
||||
return Math.max(1, 100 - this.feeCategoryPrefix.length);
|
||||
},
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.fee_item_add, false),
|
||||
@@ -418,6 +426,7 @@ export default {
|
||||
});
|
||||
},
|
||||
beforeOpen(done, type) {
|
||||
this.dialogType = type || 'add';
|
||||
if (['edit', 'view'].includes(type)) {
|
||||
getDetail(this.form.id).then(res => {
|
||||
this.form = this.normalizeFormForEdit(res.data.data || {});
|
||||
@@ -439,6 +448,13 @@ export default {
|
||||
this.onLoad(this.page, params);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
this.selectionList = list;
|
||||
},
|
||||
selectionClear() {
|
||||
this.selectionList = [];
|
||||
this.$refs.crud.toggleSelection();
|
||||
},
|
||||
currentChange(currentPage) {
|
||||
this.page.currentPage = currentPage;
|
||||
},
|
||||
@@ -454,6 +470,7 @@ export default {
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
ids: this.ids,
|
||||
[this.website.tokenHeader]: getToken(),
|
||||
};
|
||||
},
|
||||
@@ -490,6 +507,7 @@ export default {
|
||||
const data = res.data.data;
|
||||
this.page.total = data.total;
|
||||
this.data = data.records;
|
||||
this.selectionClear();
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -250,81 +250,6 @@ import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
|
||||
const DEFAULT_COUNTRY_CODE = '+86';
|
||||
const EXCLUDED_EXCEL_HEADERS = ['数据来源', '启停状态'];
|
||||
const REQUIRED_EXPORT_HEADERS = [
|
||||
'港口码头名称',
|
||||
'港口/码头名称',
|
||||
'类型',
|
||||
'国家',
|
||||
'城市',
|
||||
'经度',
|
||||
'纬度',
|
||||
];
|
||||
|
||||
const getExcelCellText = value => {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'object') {
|
||||
if (Array.isArray(value.richText)) {
|
||||
return value.richText.map(item => item.text || '').join('');
|
||||
}
|
||||
if (value.text) return value.text;
|
||||
if (value.result) return value.result;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const removeWorkbookColumnsByHeaders = workbook => {
|
||||
workbook.eachSheet(worksheet => {
|
||||
const maxHeaderRow = Math.min(worksheet.rowCount || 0, 10);
|
||||
for (let rowIndex = 1; rowIndex <= maxHeaderRow; rowIndex += 1) {
|
||||
const row = worksheet.getRow(rowIndex);
|
||||
const indexes = [];
|
||||
row.eachCell((cell, colNumber) => {
|
||||
const text = getExcelCellText(cell.value).trim();
|
||||
if (EXCLUDED_EXCEL_HEADERS.includes(text)) {
|
||||
indexes.push(colNumber);
|
||||
}
|
||||
});
|
||||
if (indexes.length) {
|
||||
indexes.sort((a, b) => b - a).forEach(colNumber => worksheet.spliceColumns(colNumber, 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addWorkbookRequiredHeaderMarks = workbook => {
|
||||
workbook.eachSheet(worksheet => {
|
||||
const maxHeaderRow = Math.min(worksheet.rowCount || 0, 10);
|
||||
for (let rowIndex = 1; rowIndex <= maxHeaderRow; rowIndex += 1) {
|
||||
const row = worksheet.getRow(rowIndex);
|
||||
let hasRequiredHeader = false;
|
||||
row.eachCell(cell => {
|
||||
const text = getExcelCellText(cell.value).trim();
|
||||
const header = text.replace(/\*+$/g, '').trim();
|
||||
if (REQUIRED_EXPORT_HEADERS.includes(header)) {
|
||||
cell.value = `${header}*`;
|
||||
hasRequiredHeader = true;
|
||||
}
|
||||
});
|
||||
if (hasRequiredHeader) break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const removeExcelColumnsByHeaders = async (blob, options = {}) => {
|
||||
const { markRequiredHeaders = false } = options;
|
||||
const ExcelJS = await import('exceljs');
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(await blob.arrayBuffer());
|
||||
removeWorkbookColumnsByHeaders(workbook);
|
||||
if (markRequiredHeaders) {
|
||||
addWorkbookRequiredHeaderMarks(workbook);
|
||||
}
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
return new Blob([buffer], { type: 'application/vnd.ms-excel' });
|
||||
};
|
||||
|
||||
const newLocal = '请选择类型';
|
||||
export default {
|
||||
data() {
|
||||
@@ -1323,9 +1248,6 @@ export default {
|
||||
'港口码头主数据',
|
||||
() => {
|
||||
this.loadPortOptions();
|
||||
},
|
||||
{
|
||||
failDetailDecorator: removeWorkbookColumnsByHeaders,
|
||||
}
|
||||
);
|
||||
},
|
||||
@@ -1339,11 +1261,8 @@ export default {
|
||||
exportBlob('/blade-system/port-terminal/export-port-terminal', this.buildExportParams(), {
|
||||
feedback: true,
|
||||
})
|
||||
.then(async res => {
|
||||
const blob = await removeExcelColumnsByHeaders(res.data, {
|
||||
markRequiredHeaders: true,
|
||||
});
|
||||
downloadXls(blob, `港口码头主数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
|
||||
.then(res => {
|
||||
downloadXls(res.data, `港口码头主数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
|
||||
})
|
||||
.finally(() => {
|
||||
NProgress.done();
|
||||
@@ -1362,9 +1281,8 @@ export default {
|
||||
`/blade-system/port-terminal/export-template?${this.website.tokenHeader}=${getToken()}`,
|
||||
undefined,
|
||||
{ feedback: true }
|
||||
).then(async res => {
|
||||
const blob = await removeExcelColumnsByHeaders(res.data);
|
||||
downloadXls(blob, '港口码头主数据模板.xlsx');
|
||||
).then(res => {
|
||||
downloadXls(res.data, '港口码头主数据模板.xlsx');
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -689,6 +689,7 @@ export default {
|
||||
feedback: true,
|
||||
}).then(res => {
|
||||
downloadXls(res.data, '行政区划模板.xlsx');
|
||||
this.$message.success('模板下载成功');
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -399,8 +399,17 @@ export default {
|
||||
if (submitRow.cargoValue !== undefined && submitRow.cargoValue !== null) {
|
||||
submitRow.cargoValue = String(submitRow.cargoValue).trim();
|
||||
}
|
||||
if (!submitRow.cargoValue || String(submitRow.cargoValue) === '-1') {
|
||||
submitRow.cargoValue = null;
|
||||
}
|
||||
return submitRow;
|
||||
},
|
||||
normalizeCargoValue(row) {
|
||||
if (row && String(row.cargoValue) === '-1') {
|
||||
row.cargoValue = null;
|
||||
}
|
||||
return row;
|
||||
},
|
||||
validateRow(row) {
|
||||
if (!row.firstCargoTypeName || !row.firstCargoTypeCode) {
|
||||
this.$message.warning('请选择一级货物类型');
|
||||
@@ -539,7 +548,7 @@ export default {
|
||||
}
|
||||
if (['edit', 'view'].includes(type)) {
|
||||
api.getDetail(this.form.id).then(res => {
|
||||
this.form = res.data.data || {};
|
||||
this.form = this.normalizeCargoValue(res.data.data || {});
|
||||
this.ensureCargoTypeOption('firstCargoTypeOptions', {
|
||||
cargoName: this.form.firstCargoTypeName,
|
||||
cargoCode: this.form.firstCargoTypeCode,
|
||||
@@ -593,7 +602,7 @@ export default {
|
||||
.then(res => {
|
||||
const result = res.data.data;
|
||||
this.page.total = result.total;
|
||||
this.data = result.records;
|
||||
this.data = (result.records || []).map(row => this.normalizeCargoValue(row));
|
||||
this.selectionClear();
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -654,6 +663,12 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.common-cargo-page {
|
||||
// 仅缩短本页面筛选栏中的输入框和下拉框,避免影响表单及其他页面。
|
||||
:deep(.avue-crud__search .el-form-item__content > .el-input),
|
||||
:deep(.avue-crud__search .el-form-item__content > .el-select) {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
&__value-unit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -303,6 +303,7 @@
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!routeMapSelected.longitude"
|
||||
:loading="routeMapConfirmLoading"
|
||||
@click="confirmRouteMapPick"
|
||||
>
|
||||
确定
|
||||
@@ -315,6 +316,7 @@
|
||||
<script>
|
||||
import * as api from '@/api/business/common-route';
|
||||
import { getList as getCommonAddressList } from '@/api/base/common-address';
|
||||
import { getDetail as getRegionDetail } from '@/api/base/region';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { addressTypeOptions } from '@/option/base/common-address';
|
||||
@@ -356,6 +358,7 @@ export default {
|
||||
routeMapVisible: false,
|
||||
routeMapTarget: '',
|
||||
routeMapLoading: false,
|
||||
routeMapConfirmLoading: false,
|
||||
routeMapKeyword: '',
|
||||
routeMapStatus: '可搜索地址或点击地图选点',
|
||||
routeMapSelected: {},
|
||||
@@ -496,14 +499,22 @@ export default {
|
||||
this.addressLoading = false;
|
||||
});
|
||||
},
|
||||
selectAddressRow(row) {
|
||||
async selectAddressRow(row) {
|
||||
if (!row) return;
|
||||
this.applyAddress(this.addressTarget, row);
|
||||
const applied = await this.applyAddress(this.addressTarget, row);
|
||||
if (!applied) return;
|
||||
this.addressDialogVisible = false;
|
||||
},
|
||||
applyAddress(target, address) {
|
||||
async applyAddress(target, address) {
|
||||
if (!target || !address) return;
|
||||
const prefix = target === 'departure' ? 'departure' : 'arrival';
|
||||
const regionIds = await this.resolveRegionIds(address.regionCode);
|
||||
if (!this.hasCompleteRegionIds(regionIds)) {
|
||||
this.$message.warning('该常用地址缺少完整的省、市、区ID,请先维护行政区划');
|
||||
return false;
|
||||
}
|
||||
this.applyRegionIds(prefix, regionIds);
|
||||
this.form[`${prefix}AddressId`] = address.id || '';
|
||||
this.form[`${prefix}Name`] = address.addressName || '';
|
||||
this.form[`${prefix}Address`] = address.detailAddress || '';
|
||||
this.form[`${prefix}AddressCode`] = address.addressCode || '';
|
||||
@@ -515,6 +526,7 @@ export default {
|
||||
this.form[`${prefix}Latitude`] = address.latitude || '';
|
||||
this.form[`${prefix}Contact`] = address.contactName || this.form[`${prefix}Contact`] || '';
|
||||
this.form[`${prefix}Phone`] = address.contactPhone || this.form[`${prefix}Phone`] || '';
|
||||
return true;
|
||||
},
|
||||
openRouteMapDialog(target) {
|
||||
if (this.dialogReadonly) return;
|
||||
@@ -526,6 +538,10 @@ export default {
|
||||
lat: this.form[`${prefix}Latitude`],
|
||||
address: this.form[`${prefix}Address`],
|
||||
regionName: this.form[`${prefix}Name`],
|
||||
regionCode: this.form[`${prefix}DistrictId`],
|
||||
provinceId: this.form[`${prefix}ProvinceId`],
|
||||
cityId: this.form[`${prefix}CityId`],
|
||||
districtId: this.form[`${prefix}DistrictId`],
|
||||
});
|
||||
this.routeMapStatus = this.routeMapSelected.longitude
|
||||
? '已加载当前选点,可重新选点'
|
||||
@@ -638,6 +654,7 @@ export default {
|
||||
...this.routeMapSelected,
|
||||
detailAddress: address.detailAddress || this.routeMapSelected.detailAddress,
|
||||
regionName: address.regionName || this.routeMapSelected.regionName,
|
||||
regionCode: address.regionCode || '',
|
||||
};
|
||||
this.routeMapKeyword = this.routeMapSelected.detailAddress || this.routeMapKeyword;
|
||||
this.routeMapStatus = this.routeMapSelected.detailAddress || '已选点,可确认回填';
|
||||
@@ -656,12 +673,23 @@ export default {
|
||||
this.routeAmapMarker.setMap(this.routeAmap);
|
||||
this.routeAmap.setCenter(point);
|
||||
},
|
||||
confirmRouteMapPick() {
|
||||
async confirmRouteMapPick() {
|
||||
if (!this.routeMapSelected.longitude) {
|
||||
this.$message.warning('请先搜索或点击地图完成选点');
|
||||
return;
|
||||
}
|
||||
const prefix = this.routeMapTarget;
|
||||
this.routeMapConfirmLoading = true;
|
||||
try {
|
||||
const regionIds = await this.resolveRegionIds(this.routeMapSelected.regionCode);
|
||||
if (!this.hasCompleteRegionIds(regionIds)) {
|
||||
this.$message.warning('当前选点未解析到完整的省、市、区ID,请重新选择地址');
|
||||
return;
|
||||
}
|
||||
this.applyRegionIds(prefix, regionIds);
|
||||
this.form[`${prefix}AddressId`] = '';
|
||||
this.form[`${prefix}AddressCode`] = '';
|
||||
this.form[`${prefix}SiteCode`] = '';
|
||||
this.form[`${prefix}Address`] =
|
||||
this.routeMapSelected.detailAddress || this.form[`${prefix}Address`];
|
||||
this.form[`${prefix}Name`] = this.routeMapSelected.regionName || this.form[`${prefix}Name`];
|
||||
@@ -669,15 +697,66 @@ export default {
|
||||
this.form[`${prefix}Latitude`] = this.routeMapSelected.latitude;
|
||||
this.routeMapVisible = false;
|
||||
this.$refs.crud?.validateField?.(`${prefix}Address`);
|
||||
} finally {
|
||||
this.routeMapConfirmLoading = false;
|
||||
}
|
||||
},
|
||||
buildRouteMapSelection({ lng, lat, address, regionName }) {
|
||||
buildRouteMapSelection({
|
||||
lng,
|
||||
lat,
|
||||
address,
|
||||
regionName,
|
||||
regionCode,
|
||||
provinceId,
|
||||
cityId,
|
||||
districtId,
|
||||
}) {
|
||||
return {
|
||||
longitude: this.formatCoordinate(lng),
|
||||
latitude: this.formatCoordinate(lat),
|
||||
detailAddress: address || '',
|
||||
regionName: regionName || '',
|
||||
regionCode: regionCode || '',
|
||||
provinceId: provinceId || '',
|
||||
cityId: cityId || '',
|
||||
districtId: districtId || '',
|
||||
};
|
||||
},
|
||||
applyRegionIds(prefix, regionIds = {}) {
|
||||
this.form[`${prefix}ProvinceId`] = regionIds.provinceId || '';
|
||||
this.form[`${prefix}CityId`] = regionIds.cityId || '';
|
||||
this.form[`${prefix}DistrictId`] = regionIds.districtId || '';
|
||||
},
|
||||
hasCompleteRegionIds(regionIds = {}) {
|
||||
return Boolean(regionIds.provinceId && regionIds.cityId && regionIds.districtId);
|
||||
},
|
||||
fallbackRegionIds(regionCode) {
|
||||
const code = String(regionCode || '').trim();
|
||||
if (!/^\d{6}$/.test(code)) {
|
||||
return { provinceId: '', cityId: '', districtId: '' };
|
||||
}
|
||||
return {
|
||||
provinceId: `${code.slice(0, 2)}0000`,
|
||||
cityId: `${code.slice(0, 4)}00`,
|
||||
districtId: code,
|
||||
};
|
||||
},
|
||||
async resolveRegionIds(regionCode) {
|
||||
const fallback = this.fallbackRegionIds(regionCode);
|
||||
if (!regionCode) return fallback;
|
||||
try {
|
||||
const res = await getRegionDetail(regionCode);
|
||||
const region = res.data.data || {};
|
||||
return {
|
||||
provinceId: String(region.provinceCode || fallback.provinceId || ''),
|
||||
cityId: String(region.cityCode || fallback.cityId || ''),
|
||||
districtId: String(region.districtCode || region.code || fallback.districtId || ''),
|
||||
};
|
||||
} catch (error) {
|
||||
window.console.log(error);
|
||||
return fallback;
|
||||
}
|
||||
},
|
||||
formatCoordinate(value) {
|
||||
if (value === undefined || value === null || value === '') return '';
|
||||
const numberValue = Number(value);
|
||||
@@ -754,6 +833,7 @@ export default {
|
||||
return {
|
||||
detailAddress: regeocode.formattedAddress || '',
|
||||
regionName: [component.province, city, component.district].filter(Boolean).join(''),
|
||||
regionCode: component.adcode || '',
|
||||
};
|
||||
},
|
||||
normalizeRow(row) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -143,7 +143,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>
|
||||
@@ -152,7 +152,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>
|
||||
@@ -192,7 +192,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';
|
||||
|
||||
@@ -263,6 +262,7 @@ export default {
|
||||
selected: false,
|
||||
documentType: '运单',
|
||||
carrierType: '承运商',
|
||||
carrierContractId: '',
|
||||
currency: 'CNY',
|
||||
otherFeeTotal: '',
|
||||
freightItems: [],
|
||||
@@ -526,28 +526,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;
|
||||
}
|
||||
@@ -605,9 +606,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('请补全非公路运输的承运信息');
|
||||
}
|
||||
}
|
||||
@@ -616,7 +617,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,
|
||||
@@ -634,8 +635,11 @@ export default {
|
||||
editPending(item) {
|
||||
const route = this.routes.find(routeItem => routeItem.segmentNo === item.segmentNo);
|
||||
if (!route) return;
|
||||
const sourceIndex = (this.master.goods || []).findIndex(goods => goods.cargoName === item.cargoName && goods.cargoType === item.cargoType);
|
||||
let goods = route.goods.find(goodsItem => goodsItem.cargoName === item.cargoName && goodsItem.cargoType === item.cargoType);
|
||||
const itemSourceIndex = Number(item.sourceIndex);
|
||||
const sourceIndex = Number.isInteger(itemSourceIndex) && itemSourceIndex >= 0
|
||||
? itemSourceIndex
|
||||
: (this.master.goods || []).findIndex(goods => goods.cargoName === item.cargoName && goods.cargoType === item.cargoType);
|
||||
let goods = route.goods.find(goodsItem => String(goodsItem.sourceIndex) === String(sourceIndex));
|
||||
if (!goods && sourceIndex >= 0) {
|
||||
goods = this.createGoodsRow(this.master.goods[sourceIndex], sourceIndex);
|
||||
route.goods.push(goods);
|
||||
@@ -645,8 +649,9 @@ export default {
|
||||
this.syncFreightItems(route);
|
||||
const freightItem = this.freightItemsForGoods(route, goods);
|
||||
if (freightItem) { freightItem.unitPrice = item.unitPrice || ''; freightItem.priceUnit = item.priceUnit || freightItem.priceUnit; }
|
||||
this.editingId = item.id;
|
||||
this.removePending(item.id);
|
||||
this.editingId = item.id;
|
||||
route.selected = true;
|
||||
this.$nextTick(() => document.querySelector(`[data-segment="${item.segmentNo}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
|
||||
},
|
||||
freightItemsForGoods(route, goods) {
|
||||
|
||||
@@ -875,6 +875,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;
|
||||
@@ -925,7 +929,8 @@ export default {
|
||||
async loadContracts(projectId, selectFirst = true) {
|
||||
const response = await api.getProjectContracts(projectId);
|
||||
this.contracts = this.responseRecords(response).filter(
|
||||
item => String(item.projectId) === String(projectId)
|
||||
item =>
|
||||
String(item.projectId) === String(projectId) && item.contractCategory === '客户合同'
|
||||
);
|
||||
if (selectFirst) {
|
||||
const first = this.contracts[0];
|
||||
@@ -1711,8 +1716,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}` })),
|
||||
@@ -1722,6 +1727,10 @@ export default {
|
||||
departureAddress: this.form.arrivalAddress,
|
||||
departureContact: this.form.arrivalContact,
|
||||
departurePhone: this.form.arrivalPhone,
|
||||
departureLongitude: this.form.arrivalLongitude,
|
||||
departureLatitude: this.form.arrivalLatitude,
|
||||
departureRegionCode: this.form.arrivalRegionCode,
|
||||
departureSiteCode: this.form.arrivalSiteCode,
|
||||
transportType: this.form.finalTransportType,
|
||||
},
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -999,6 +1002,7 @@ const defaultTransportType = '';
|
||||
const defaultCandidateTransportType = '';
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills';
|
||||
let detailAmapLoader;
|
||||
|
||||
const createSearchForm = () => ({
|
||||
@@ -1054,6 +1058,7 @@ const createDialogForm = () => ({
|
||||
driverPhone: '',
|
||||
carrierType: '承运商',
|
||||
carrierName: '',
|
||||
carrierContractId: '',
|
||||
escortName: '',
|
||||
escortPhone: '',
|
||||
departureAddress: '',
|
||||
@@ -1107,6 +1112,18 @@ const parseJsonArray = value => {
|
||||
}
|
||||
};
|
||||
|
||||
const parseIdJsonArray = value => {
|
||||
if (Array.isArray(value)) return value.map(id => String(id));
|
||||
if (typeof value !== 'string' || !value.trim()) return [];
|
||||
try {
|
||||
const normalizedValue = value.replace(/(^\s*\[\s*|,\s*)(-?\d{16,})(\s*(?=,|\]))/g, '$1"$2"$3');
|
||||
const result = JSON.parse(normalizedValue);
|
||||
return Array.isArray(result) ? result.map(id => String(id)) : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const uniqueText = (rows, prop) =>
|
||||
[
|
||||
...new Set(
|
||||
@@ -1165,13 +1182,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 +1219,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: '新建配载',
|
||||
@@ -1349,7 +1374,7 @@ export default {
|
||||
if (linkedRows[index]?.id || linkedRows[index]?.waybillId) {
|
||||
return linkedRows[index].id || linkedRows[index].waybillId;
|
||||
}
|
||||
const ids = parseJsonArray(row.waybillIdsJson);
|
||||
const ids = parseIdJsonArray(row.waybillIdsJson);
|
||||
return ids[index] || '';
|
||||
},
|
||||
async openWaybillDetail(row = {}) {
|
||||
@@ -1533,11 +1558,25 @@ export default {
|
||||
};
|
||||
actionMap[type]?.();
|
||||
},
|
||||
initStandaloneFormPage() {
|
||||
async initStandaloneFormPage() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
this.openLoadingDialog(this.$route.query.mode === 'edit' ? 'edit' : 'add', {
|
||||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||
await this.openLoadingDialog(mode, {
|
||||
id: this.$route.query.id,
|
||||
});
|
||||
if (mode === 'add') await this.consumeLoadingCreateWaybills();
|
||||
},
|
||||
async consumeLoadingCreateWaybills() {
|
||||
const raw = sessionStorage.getItem(loadingCreateWaybillsStorageKey);
|
||||
if (!raw) return;
|
||||
sessionStorage.removeItem(loadingCreateWaybillsStorageKey);
|
||||
try {
|
||||
const payload = JSON.parse(raw);
|
||||
if (payload?.source !== 'waybill-manage' || !Array.isArray(payload.rows)) return;
|
||||
await this.addWaybillRows(payload.rows);
|
||||
} catch (error) {
|
||||
ElMessage.warning('待配载运单数据读取失败,请重新选择');
|
||||
}
|
||||
},
|
||||
async openLoadingDialog(mode, row) {
|
||||
if (['add', 'edit'].includes(mode) && !this.isStandaloneFormPage) {
|
||||
@@ -1567,6 +1606,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 +1647,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;
|
||||
@@ -1844,7 +1890,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async restoreWaybillRows(idsJson, loadingSubNos) {
|
||||
const ids = parseJsonArray(idsJson).filter(Boolean);
|
||||
const ids = parseIdJsonArray(idsJson).filter(Boolean);
|
||||
if (ids.length) {
|
||||
const rows = await Promise.all(
|
||||
ids.map(id =>
|
||||
@@ -1855,10 +1901,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;
|
||||
@@ -1906,11 +1954,13 @@ export default {
|
||||
candidateSelectionChange(rows) {
|
||||
this.candidateSelection = rows || [];
|
||||
},
|
||||
async addSelectedWaybills() {
|
||||
async addWaybillRows(rows = []) {
|
||||
const waybillRows = Array.isArray(rows) ? rows.filter(Boolean) : [];
|
||||
if (!waybillRows.length) return;
|
||||
this.candidateAdding = true;
|
||||
try {
|
||||
const selectedRows = await Promise.all(
|
||||
this.candidateSelection.map(async row => {
|
||||
waybillRows.map(async row => {
|
||||
if (!row.id) return row;
|
||||
try {
|
||||
const res = await getWaybillDetail(row.id);
|
||||
@@ -1933,15 +1983,20 @@ export default {
|
||||
});
|
||||
this.selectedWaybillRows = nextRows;
|
||||
this.rebuildSummaryFromWaybills();
|
||||
await this.loadTaskCarrierOptions();
|
||||
this.$refs.candidateTableRef?.clearSelection?.();
|
||||
this.candidateSelection = [];
|
||||
} finally {
|
||||
this.candidateAdding = false;
|
||||
}
|
||||
},
|
||||
async addSelectedWaybills() {
|
||||
await this.addWaybillRows(this.candidateSelection);
|
||||
},
|
||||
removeSelectedWaybill(index) {
|
||||
this.selectedWaybillRows.splice(index, 1);
|
||||
this.rebuildSummaryFromWaybills();
|
||||
this.loadTaskCarrierOptions();
|
||||
},
|
||||
rebuildSummaryFromWaybills(rebuildRoute = true) {
|
||||
const rows = this.selectedWaybillRows;
|
||||
@@ -1949,7 +2004,12 @@ export default {
|
||||
.map(row => row.waybillNo)
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
this.dialogForm.waybillIdsJson = JSON.stringify(rows.map(row => row.id).filter(Boolean));
|
||||
this.dialogForm.waybillIdsJson = JSON.stringify(
|
||||
rows
|
||||
.map(row => row.id)
|
||||
.filter(Boolean)
|
||||
.map(id => String(id))
|
||||
);
|
||||
this.dialogForm.projectName = uniqueText(rows, 'projectName');
|
||||
this.dialogForm.customerName = uniqueText(rows, 'customerName');
|
||||
this.dialogForm.originalNo = uniqueText(rows, 'originalNo');
|
||||
@@ -2071,6 +2131,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),
|
||||
@@ -2111,7 +2175,7 @@ export default {
|
||||
if (draftMode) {
|
||||
return true;
|
||||
}
|
||||
if (this.dialogMode === 'add' && this.selectedWaybillRows.length < 2) {
|
||||
if (this.selectedWaybillRows.length < 2) {
|
||||
ElMessage.warning('请至少选择两条待配载运单');
|
||||
return false;
|
||||
}
|
||||
@@ -2130,14 +2194,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 +2282,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 +2315,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 +2420,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 {
|
||||
|
||||
@@ -797,11 +797,25 @@ export default {
|
||||
result.push(
|
||||
`是否接单:${node.confirmMode === 'no_confirm_accept' ? '无需确认接单' : '是'}`
|
||||
);
|
||||
result.push(
|
||||
`确认接单人(司机):${node.confirmMode === 'yes' && node.confirmDriver ? '是' : '否'}`
|
||||
);
|
||||
return result;
|
||||
}
|
||||
if (node.key === 'return') {
|
||||
result.push(
|
||||
`是否确认:${node.confirmMode === 'no_confirm_complete' ? '无需确认完成' : '是'}`
|
||||
);
|
||||
result.push(
|
||||
`确认完成(内部人员):${
|
||||
node.confirmMode === 'yes' && node.confirmInternal ? '是' : '否'
|
||||
}`
|
||||
);
|
||||
if (node.supportVoucher) {
|
||||
result.push(`上传凭证:${node.uploadVoucher ? '是' : '否'}`);
|
||||
if (node.uploadVoucher && node.voucherTypes.length) {
|
||||
result.push(`上传凭证:${node.voucherTypes.join('、')}`);
|
||||
result.push(`凭证类型:${node.voucherTypes.join('、')}`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -816,11 +830,17 @@ export default {
|
||||
result.push(`打卡操作:${node.punch ? '是' : '否'}`);
|
||||
result.push(`打卡定位:${node.location ? '是' : '否'}`);
|
||||
}
|
||||
if (node.supportCargo && node.uploadCargo && node.cargoTypes.length) {
|
||||
result.push(`上传货量:${node.cargoTypes.join('、')}`);
|
||||
if (node.supportCargo) {
|
||||
result.push(`上传货量:${node.uploadCargo ? '是' : '否'}`);
|
||||
if (node.uploadCargo && node.cargoTypes.length) {
|
||||
result.push(`上传货量类型:${node.cargoTypes.join('、')}`);
|
||||
}
|
||||
}
|
||||
if (node.supportVoucher) {
|
||||
result.push(`上传凭证:${node.uploadVoucher ? '是' : '否'}`);
|
||||
if (node.uploadVoucher && node.voucherTypes.length) {
|
||||
result.push(`凭证类型:${node.voucherTypes.join('、')}`);
|
||||
}
|
||||
if (node.supportVoucher && node.uploadVoucher && node.voucherTypes.length) {
|
||||
result.push(`上传凭证:${node.voucherTypes.join('、')}`);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -19,12 +23,29 @@
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'project' && editable"
|
||||
v-model="form.projectId"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择项目"
|
||||
:disabled="details.length > 0 || sources.length > 0"
|
||||
@change="handleProjectChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in projects"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-else-if="field.type === 'contract' && editable"
|
||||
v-model="form.contractId"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="loadContracts"
|
||||
:disabled="!form.projectId || details.length > 0 || sources.length > 0"
|
||||
placeholder="请选择合同"
|
||||
@change="handleContractChange"
|
||||
>
|
||||
@@ -46,6 +67,9 @@
|
||||
<span v-else-if="field.type === 'contract'">{{
|
||||
displayValue(form.contractName)
|
||||
}}</span>
|
||||
<span v-else-if="field.type === 'project'">{{
|
||||
displayValue(form.projectName)
|
||||
}}</span>
|
||||
<span v-else>{{ displayValue(form[field.prop]) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -65,40 +89,121 @@
|
||||
</el-form>
|
||||
</section-card>
|
||||
|
||||
<section-card title="来源预结算单">
|
||||
<section-card title="结算合计">
|
||||
<template #extra>
|
||||
<div class="formal-editor__actions">
|
||||
<el-button v-if="editable" type="primary" plain @click="openCandidateDialog"
|
||||
>选择预结算单</el-button
|
||||
>
|
||||
<el-button v-if="editable" type="primary" plain @click="openDetailDialog"
|
||||
>选择结算明细</el-button
|
||||
>
|
||||
</div>
|
||||
<el-button v-if="editable" type="primary" plain @click="addSummaryFee">
|
||||
添加费用
|
||||
</el-button>
|
||||
</template>
|
||||
<el-table :data="sources" border>
|
||||
<el-table :data="summaryFees" border show-summary :summary-method="getSummarySums">
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in sourceTableColumns"
|
||||
v-for="column in summaryTableColumns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
:label="column.label"
|
||||
:prop="column.prop"
|
||||
:min-width="column.minWidth"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<el-select
|
||||
v-if="editable && column.prop === 'feeType' && row.manualFlag === 1"
|
||||
v-model="row.feeType"
|
||||
filterable
|
||||
placeholder="请选择"
|
||||
@change="handleManualFeeTypeChange(row)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in feeOptions"
|
||||
:key="item.feeType"
|
||||
:label="item.feeTypeName || item.feeType"
|
||||
:value="item.feeType"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-else-if="editable && column.prop === 'feeItem' && row.manualFlag === 1"
|
||||
v-model="row.feeItem"
|
||||
filterable
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="name in manualFeeItems(row.feeType)"
|
||||
:key="name"
|
||||
:label="name"
|
||||
:value="name"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input-number
|
||||
v-else-if="editable && column.prop === 'adjustAmount'"
|
||||
v-model="row.adjustAmount"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
@change="recalculateSummaryRow(row)"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="editable && column.prop === 'remark'"
|
||||
v-model="row.remark"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
<span v-else-if="isSummaryMoney(column.prop)">
|
||||
{{ formatMoney(row[column.prop]) }}
|
||||
</span>
|
||||
<span v-else-if="column.prop === 'feeType'">{{ feeCategoryName(row.feeType) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="editable" label="操作" width="90" align="center">
|
||||
<template #default="{ $index }"
|
||||
><el-link type="danger" @click="removeSource($index)">删除</el-link></template
|
||||
>
|
||||
<el-table-column v-if="editable" label="操作" width="100" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<el-link v-if="row.manualFlag === 1" type="danger" @click="removeSummaryFee($index)">
|
||||
删除
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section-card>
|
||||
|
||||
<section-card v-if="details.length" title="结算明细">
|
||||
<el-table :data="details" border>
|
||||
<section-card title="结算明细">
|
||||
<template #extra>
|
||||
<div class="formal-editor__actions">
|
||||
<el-button type="primary" text @click="detailCollapsed = !detailCollapsed">
|
||||
{{ detailCollapsed ? '展开明细' : '收起明细' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-form
|
||||
:model="detailQuery"
|
||||
inline
|
||||
label-position="right"
|
||||
label-width="160px"
|
||||
class="formal-editor__detail-filter"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form-item label="单据号">
|
||||
<el-input v-model="detailQuery.documentNo" clearable @keyup.enter="handleDetailQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="运单号">
|
||||
<el-input v-model="detailQuery.waybillNo" clearable @keyup.enter="handleDetailQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="批次号">
|
||||
<el-input v-model="detailQuery.batchNo" clearable @keyup.enter="handleDetailQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="货物名称">
|
||||
<el-input v-model="detailQuery.cargoName" clearable @keyup.enter="handleDetailQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item class="formal-editor__detail-filter-actions">
|
||||
<el-button type="primary" @click="handleDetailQuery">查询</el-button>
|
||||
<el-button @click="resetDetailQuery">重置</el-button>
|
||||
<el-button type="primary" :disabled="!details.length" @click="exportDetails">
|
||||
导出
|
||||
</el-button>
|
||||
<el-button v-if="editable" type="primary" @click="openDetailDialog">
|
||||
选择结算明细
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-show="!detailCollapsed" :data="filteredDetails" border>
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in detailTableColumns"
|
||||
@@ -113,15 +218,12 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="editable" label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<template #default="{ row }">
|
||||
<div class="formal-editor__links">
|
||||
<el-link v-if="row.formalSettlementId" type="primary" @click="openAdjustDialog(row)"
|
||||
>调整</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="!row.sourcePreSettlementId"
|
||||
type="danger"
|
||||
@click="removeDetail($index)"
|
||||
<el-link v-if="!row.sourcePreSettlementId" type="danger" @click="removeDetail(row)"
|
||||
>删除</el-link
|
||||
>
|
||||
</div>
|
||||
@@ -164,15 +266,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,15 +336,22 @@
|
||||
</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>
|
||||
<el-form-item label="费用日期"
|
||||
<el-form-item label="创建时间"
|
||||
><el-date-picker
|
||||
v-model="detailCandidate.query.feeDateRange"
|
||||
v-model="detailCandidate.query.createTimeRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/></el-form-item>
|
||||
@@ -344,7 +465,7 @@
|
||||
></template
|
||||
>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -355,8 +476,11 @@ import {
|
||||
getContractOptions,
|
||||
getDetail,
|
||||
getDetailFees,
|
||||
getFeeOptions,
|
||||
getNextNo,
|
||||
save,
|
||||
} from '@/api/settlement/formalSettlement';
|
||||
import { getDetail as getPreSettlementDetail } from '@/api/settlement/preSettlement';
|
||||
import {
|
||||
createFormalSettlementForm,
|
||||
formalSettlementFormFields,
|
||||
@@ -367,11 +491,22 @@ import {
|
||||
detailColumns,
|
||||
paymentColumns,
|
||||
sourceColumns,
|
||||
summaryColumns,
|
||||
} from '@/option/settlement/formalSettlementTable';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
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 {
|
||||
@@ -380,6 +515,7 @@ export default {
|
||||
form: createFormalSettlementForm(),
|
||||
sources: [],
|
||||
details: [],
|
||||
summaryFees: [],
|
||||
payments: [],
|
||||
attachments: [],
|
||||
attachmentFileTypes: [
|
||||
@@ -400,13 +536,18 @@ export default {
|
||||
'rar',
|
||||
],
|
||||
contracts: [],
|
||||
allContracts: [],
|
||||
projects: [],
|
||||
feeOptions: [],
|
||||
fields: formalSettlementFormFields,
|
||||
sourceTableColumns: sourceColumns,
|
||||
summaryTableColumns: summaryColumns,
|
||||
detailTableColumns: detailColumns,
|
||||
paymentTableColumns: paymentColumns,
|
||||
candidateTableColumns: candidateColumns,
|
||||
candidateDetailTableColumns: candidateDetailColumns,
|
||||
rules: {
|
||||
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
|
||||
contractId: [{ required: true, message: '请选择合同', trigger: 'change' }],
|
||||
exchangeRateDate: [{ required: true, message: '请选择汇率日期', trigger: 'change' }],
|
||||
exchangeRate: [{ required: true, message: '请输入结算汇率', trigger: 'blur' }],
|
||||
@@ -422,7 +563,7 @@ export default {
|
||||
detailCandidate: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
query: {},
|
||||
query: { batchNo: '', createTimeRange: [] },
|
||||
rows: [],
|
||||
selected: [],
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
@@ -435,6 +576,19 @@ export default {
|
||||
reason: '',
|
||||
rows: [],
|
||||
},
|
||||
detailCollapsed: false,
|
||||
detailQuery: {
|
||||
documentNo: '',
|
||||
waybillNo: '',
|
||||
batchNo: '',
|
||||
cargoName: '',
|
||||
},
|
||||
appliedDetailQuery: {
|
||||
documentNo: '',
|
||||
waybillNo: '',
|
||||
batchNo: '',
|
||||
cargoName: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -446,28 +600,77 @@ 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;
|
||||
},
|
||||
title() {
|
||||
return this.readonly ? '查看正式结算单' : this.recordId ? '编辑正式结算单' : '新增正式结算单';
|
||||
},
|
||||
summaryTotal() {
|
||||
return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0);
|
||||
},
|
||||
filteredDetails() {
|
||||
return this.details.filter(row =>
|
||||
Object.entries(this.appliedDetailQuery).every(([field, keyword]) => {
|
||||
if (!keyword?.trim()) return true;
|
||||
return String(row[field] || '')
|
||||
.toLocaleLowerCase()
|
||||
.includes(keyword.trim().toLocaleLowerCase());
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
modelValue: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
if (value) this.initialize();
|
||||
},
|
||||
},
|
||||
summaryTotal(value) {
|
||||
this.form.settlementAmount = Number(value || 0).toFixed(2);
|
||||
this.form.localSettlementAmount = (
|
||||
Number(value || 0) * Number(this.form.exchangeRate || 1)
|
||||
).toFixed(2);
|
||||
},
|
||||
'form.exchangeRate'(value) {
|
||||
this.form.localSettlementAmount = (
|
||||
Number(this.summaryTotal || 0) * Number(value || 1)
|
||||
).toFixed(2);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.form = createFormalSettlementForm();
|
||||
this.sources = [];
|
||||
this.details = [];
|
||||
this.summaryFees = [];
|
||||
this.payments = [];
|
||||
this.attachments = [];
|
||||
await this.loadContracts();
|
||||
this.detailCollapsed = false;
|
||||
this.resetDetailQuery(false);
|
||||
await Promise.all([this.loadAllContracts(), this.loadFeeOptions()]);
|
||||
if (!this.recordId) {
|
||||
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
|
||||
const response = await getNextNo();
|
||||
this.form.formalSettlementNo = this.unwrapData(response) || '';
|
||||
if (this.initialData) await this.applyInitialData();
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
@@ -484,25 +687,206 @@ export default {
|
||||
};
|
||||
this.sources = data.sources || [];
|
||||
this.details = data.details || [];
|
||||
this.summaryFees = data.summaryFees || [];
|
||||
this.payments = data.payments || [];
|
||||
this.attachments = this.parseAttachments(data.attachmentsJson);
|
||||
this.contracts = this.allContracts.filter(
|
||||
item => String(item.projectId) === String(this.form.projectId)
|
||||
);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async applyInitialData() {
|
||||
const sourcePreSettlements = Array.isArray(this.initialData?.sourcePreSettlements)
|
||||
? this.initialData.sourcePreSettlements
|
||||
: [];
|
||||
if (sourcePreSettlements.length) {
|
||||
await this.applyInitialSources(sourcePreSettlements);
|
||||
return;
|
||||
}
|
||||
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 matchedContract = this.allContracts.find(
|
||||
item =>
|
||||
(first.contractId && String(item.id) === String(first.contractId)) ||
|
||||
(first.contractNo && String(item.contractNo) === String(first.contractNo))
|
||||
);
|
||||
const contractId = first.contractId || matchedContract?.id || null;
|
||||
const projectId = first.projectId || matchedContract?.projectId || null;
|
||||
const projectName = first.projectName || matchedContract?.projectName || '';
|
||||
this.contracts = this.allContracts.filter(
|
||||
item => projectId && String(item.projectId) === String(projectId)
|
||||
);
|
||||
if (projectId && !this.projects.some(item => String(item.id) === String(projectId))) {
|
||||
this.projects.push({ id: projectId, name: projectName });
|
||||
}
|
||||
if (!this.contracts.some(item => String(item.id) === String(contractId))) {
|
||||
this.contracts.push({
|
||||
id: contractId,
|
||||
contractNo: first.contractNo,
|
||||
contractName: first.contractName,
|
||||
projectId,
|
||||
projectName,
|
||||
deptId: first.deptId || matchedContract?.deptId,
|
||||
deptName: first.deptName || matchedContract?.deptName,
|
||||
payerName: first.payerName,
|
||||
payeeName: first.payeeName,
|
||||
partyA: matchedContract?.partyA,
|
||||
partyB: matchedContract?.partyB,
|
||||
settlementType,
|
||||
});
|
||||
}
|
||||
this.handleContractChange(contractId);
|
||||
Object.assign(this.form, {
|
||||
contractId,
|
||||
contractNo: first.contractNo || this.form.contractNo,
|
||||
contractName: first.contractName || this.form.contractName,
|
||||
projectId: projectId || this.form.projectId,
|
||||
projectName: 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.buildSummaryFeesFromDetails();
|
||||
},
|
||||
async applyInitialSources(rows) {
|
||||
const first = rows[0];
|
||||
const contractId = first.contractId || null;
|
||||
const settlementType = first.settlementType || 'payable';
|
||||
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);
|
||||
this.sources = rows.map(row => ({
|
||||
...row,
|
||||
preSettlementId: row.preSettlementId || row.id,
|
||||
}));
|
||||
this.form.sourcePreSettlementIds = this.sources.map(item => item.preSettlementId);
|
||||
Object.assign(this.form, first, {
|
||||
id: null,
|
||||
formalSettlementNo: this.form.formalSettlementNo,
|
||||
contractId,
|
||||
settlementType,
|
||||
settlementTypeName: settlementType === 'receivable' ? '应收' : '应付',
|
||||
sourcePreSettlementIds: this.form.sourcePreSettlementIds,
|
||||
});
|
||||
const responses = await Promise.all(
|
||||
this.sources.map(item => getPreSettlementDetail(item.preSettlementId))
|
||||
);
|
||||
const settlements = responses.map(response => this.unwrapData(response) || {});
|
||||
this.details = settlements.flatMap(settlement =>
|
||||
(settlement.details || []).map(detail => ({
|
||||
...detail,
|
||||
sourcePreSettlementId: settlement.id,
|
||||
}))
|
||||
);
|
||||
const summaryMap = new Map();
|
||||
settlements
|
||||
.flatMap(settlement => settlement.summaryFees || [])
|
||||
.forEach(row => {
|
||||
const key = `${row.feeType}\u0000${row.feeItem}`;
|
||||
const current = summaryMap.get(key) || {
|
||||
feeType: row.feeType,
|
||||
feeItem: row.feeItem,
|
||||
originalAmount: 0,
|
||||
adjustAmount: 0,
|
||||
settlementAmount: 0,
|
||||
remark: '',
|
||||
manualFlag: 0,
|
||||
};
|
||||
current.originalAmount += Number(row.settlementAmount || 0);
|
||||
current.settlementAmount = current.originalAmount;
|
||||
summaryMap.set(key, current);
|
||||
});
|
||||
this.summaryFees = [...summaryMap.values()];
|
||||
if (!this.summaryFees.length) this.buildSummaryFeesFromDetails();
|
||||
},
|
||||
async loadAllContracts() {
|
||||
const response = await getContractOptions('');
|
||||
this.allContracts = this.unwrapData(response) || [];
|
||||
const projectMap = new Map();
|
||||
this.allContracts.forEach(item => {
|
||||
if (item.projectId)
|
||||
projectMap.set(String(item.projectId), { id: item.projectId, name: item.projectName });
|
||||
});
|
||||
this.projects = [...projectMap.values()];
|
||||
this.contracts = [];
|
||||
},
|
||||
async loadContracts(keyword = '') {
|
||||
const response = await getContractOptions(keyword);
|
||||
const projectId = this.form.projectId;
|
||||
if (!projectId) {
|
||||
this.contracts = [];
|
||||
return;
|
||||
}
|
||||
const response = await getContractOptions(keyword, projectId);
|
||||
if (String(this.form.projectId) === String(projectId)) {
|
||||
this.contracts = this.unwrapData(response) || [];
|
||||
}
|
||||
},
|
||||
async loadFeeOptions() {
|
||||
const response = await getFeeOptions();
|
||||
this.feeOptions = this.unwrapData(response) || [];
|
||||
},
|
||||
handleProjectChange(id) {
|
||||
const project = this.projects.find(item => String(item.id) === String(id));
|
||||
this.form.projectName = project?.name || '';
|
||||
this.form.contractId = null;
|
||||
this.form.contractNo = '';
|
||||
this.form.contractName = '';
|
||||
this.form.deptId = null;
|
||||
this.form.deptName = '';
|
||||
this.form.payerName = '';
|
||||
this.form.payeeName = '';
|
||||
this.sources = [];
|
||||
this.details = [];
|
||||
this.summaryFees = [];
|
||||
this.form.sourcePreSettlementIds = [];
|
||||
this.form.sourceDetailIds = [];
|
||||
this.loadContracts();
|
||||
},
|
||||
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,
|
||||
payerName:
|
||||
contract.payerName ||
|
||||
(contract.settlementType === 'receivable' ? contract.partyB : contract.partyA),
|
||||
payeeName:
|
||||
contract.payeeName ||
|
||||
(contract.settlementType === 'receivable' ? contract.partyA : contract.partyB),
|
||||
settlementTypeName: contract.settlementType === 'receivable' ? '应收' : '应付',
|
||||
});
|
||||
this.sources = [];
|
||||
this.details = [];
|
||||
this.summaryFees = [];
|
||||
this.form.sourcePreSettlementIds = [];
|
||||
this.form.sourceDetailIds = [];
|
||||
},
|
||||
@@ -566,18 +950,19 @@ export default {
|
||||
if (!this.form.contractId) return this.$message.warning('请先选择合同');
|
||||
this.detailCandidate.visible = true;
|
||||
this.detailCandidate.page.current = 1;
|
||||
this.detailCandidate.selected = [];
|
||||
this.loadDetailCandidates();
|
||||
},
|
||||
async loadDetailCandidates() {
|
||||
this.detailCandidate.loading = true;
|
||||
try {
|
||||
const range = this.detailCandidate.query.feeDateRange || [];
|
||||
const range = this.detailCandidate.query.createTimeRange || [];
|
||||
const params = {
|
||||
contractId: this.form.contractId,
|
||||
settlementType: this.form.settlementType,
|
||||
batchNo: this.detailCandidate.query.batchNo,
|
||||
feeStartDate: range[0],
|
||||
feeEndDate: range[1],
|
||||
createStartDate: range[0],
|
||||
createEndDate: range[1],
|
||||
};
|
||||
const response = await getCandidateDetails(
|
||||
this.detailCandidate.page.current,
|
||||
@@ -592,11 +977,14 @@ export default {
|
||||
}
|
||||
},
|
||||
resetDetailCandidates() {
|
||||
this.detailCandidate.query = {};
|
||||
this.detailCandidate.query = { batchNo: '', createTimeRange: [] };
|
||||
this.detailCandidate.page.current = 1;
|
||||
this.loadDetailCandidates();
|
||||
},
|
||||
confirmDetailCandidates() {
|
||||
if (!this.detailCandidate.selected.length) {
|
||||
return this.$message.warning('请选择结算明细');
|
||||
}
|
||||
const existing = new Map(
|
||||
this.details
|
||||
.filter(item => !item.formalSettlementId)
|
||||
@@ -614,16 +1002,141 @@ export default {
|
||||
...existing.values(),
|
||||
];
|
||||
this.form.sourceDetailIds = [...existing.values()].map(item => item.sourceDetailId);
|
||||
this.form.settlementAmount =
|
||||
this.sources.reduce((sum, item) => sum + Number(item.settlementAmount || 0), 0) +
|
||||
[...existing.values()].reduce((sum, item) => sum + Number(item.totalAmount || 0), 0);
|
||||
this.buildSummaryFeesFromDetails();
|
||||
this.detailCandidate.visible = false;
|
||||
},
|
||||
removeDetail(index) {
|
||||
removeDetail(row) {
|
||||
const index = this.details.indexOf(row);
|
||||
if (index < 0) return;
|
||||
this.details.splice(index, 1);
|
||||
this.form.sourceDetailIds = this.details
|
||||
.filter(item => !item.sourcePreSettlementId)
|
||||
.map(item => item.sourceDetailId);
|
||||
this.buildSummaryFeesFromDetails();
|
||||
},
|
||||
addSummaryFee() {
|
||||
this.summaryFees.push({
|
||||
feeType: '',
|
||||
feeItem: '',
|
||||
originalAmount: 0,
|
||||
adjustAmount: 0,
|
||||
settlementAmount: 0,
|
||||
remark: '',
|
||||
manualFlag: 1,
|
||||
});
|
||||
},
|
||||
removeSummaryFee(index) {
|
||||
this.summaryFees.splice(index, 1);
|
||||
},
|
||||
handleManualFeeTypeChange(row) {
|
||||
if (!this.manualFeeItems(row.feeType).includes(row.feeItem)) row.feeItem = '';
|
||||
},
|
||||
manualFeeItems(feeType) {
|
||||
return this.feeOptions.find(item => item.feeType === feeType)?.feeItems || [];
|
||||
},
|
||||
feeCategoryName(value) {
|
||||
return (
|
||||
this.feeOptions.find(item => String(item.feeType) === String(value))?.feeTypeName || value
|
||||
);
|
||||
},
|
||||
isSummaryMoney(prop) {
|
||||
return ['originalAmount', 'adjustAmount', 'settlementAmount'].includes(prop);
|
||||
},
|
||||
recalculateSummaryRow(row) {
|
||||
row.settlementAmount = Number(row.originalAmount || 0) + Number(row.adjustAmount || 0);
|
||||
},
|
||||
getSummarySums({ columns, data }) {
|
||||
const sumProps = ['originalAmount', 'adjustAmount', 'settlementAmount'];
|
||||
return columns.map((column, index) => {
|
||||
if (index === 0) return '合计';
|
||||
if (!sumProps.includes(column.property)) return '';
|
||||
const total = data.reduce((sum, row) => sum + Number(row[column.property] || 0), 0);
|
||||
return this.formatMoney(total);
|
||||
});
|
||||
},
|
||||
buildSummaryFeesFromDetails() {
|
||||
const generatedMap = new Map();
|
||||
const existingGenerated = new Map(
|
||||
this.summaryFees
|
||||
.filter(row => row.manualFlag !== 1)
|
||||
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
|
||||
);
|
||||
const append = (feeItem, value, fallbackFeeType = '') => {
|
||||
const amount = Number(value || 0);
|
||||
if (!feeItem || !Number.isFinite(amount) || Math.abs(amount) < 0.005) return;
|
||||
const option = this.feeOptions.find(item =>
|
||||
(item.feeItems || []).some(name => String(name) === String(feeItem))
|
||||
);
|
||||
const feeType =
|
||||
option?.feeType ||
|
||||
fallbackFeeType ||
|
||||
(this.isFreightFeeItem(feeItem) ? '物流配送' : '其他费用');
|
||||
const key = `${feeType}\u0000${feeItem}`;
|
||||
const old = existingGenerated.get(key);
|
||||
const current = generatedMap.get(key) || {
|
||||
id: old?.id,
|
||||
feeType,
|
||||
feeItem,
|
||||
originalAmount: 0,
|
||||
adjustAmount: Number(old?.adjustAmount || 0),
|
||||
settlementAmount: 0,
|
||||
remark: old?.remark || '',
|
||||
manualFlag: 0,
|
||||
};
|
||||
current.originalAmount = Number((current.originalAmount + amount).toFixed(2));
|
||||
current.settlementAmount = Number(
|
||||
(current.originalAmount + Number(current.adjustAmount || 0)).toFixed(2)
|
||||
);
|
||||
generatedMap.set(key, current);
|
||||
};
|
||||
this.details.forEach(row => {
|
||||
const feeItems = this.parseFeeItems(row.feeItemsJson || row.feeItems);
|
||||
const entries = Object.entries(feeItems);
|
||||
entries.forEach(([feeItem, amount]) => append(feeItem, amount, row.feeType));
|
||||
let knownAmount = entries.reduce((total, [, amount]) => total + Number(amount || 0), 0);
|
||||
if (!entries.some(([feeItem]) => this.isFreightFeeItem(feeItem))) {
|
||||
append('运输费', row.freightAmount, '物流配送');
|
||||
knownAmount += Number(row.freightAmount || 0);
|
||||
}
|
||||
const totalAmount = Number(
|
||||
row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount ?? 0
|
||||
);
|
||||
const residualAmount = Number((totalAmount - knownAmount).toFixed(2));
|
||||
if (Math.abs(residualAmount) >= 0.005) append('其他费用', residualAmount, '其他费用');
|
||||
});
|
||||
const manualRows = this.summaryFees.filter(row => row.manualFlag === 1);
|
||||
this.summaryFees = [...generatedMap.values(), ...manualRows];
|
||||
},
|
||||
isFreightFeeItem(name) {
|
||||
return name && (name.includes('运费') || name.includes('运输费'));
|
||||
},
|
||||
handleDetailQuery() {
|
||||
this.appliedDetailQuery = { ...this.detailQuery };
|
||||
},
|
||||
resetDetailQuery(apply = true) {
|
||||
this.detailQuery = { documentNo: '', waybillNo: '', batchNo: '', cargoName: '' };
|
||||
if (apply) this.handleDetailQuery();
|
||||
else this.appliedDetailQuery = { ...this.detailQuery };
|
||||
},
|
||||
exportDetails() {
|
||||
if (!this.details.length) return this.$message.warning('暂无可导出的结算明细');
|
||||
const rows = this.details.map((row, index) => {
|
||||
const result = { 序号: index + 1 };
|
||||
this.detailTableColumns.forEach(column => {
|
||||
result[column.label] = column.money
|
||||
? Number(row[column.prop] || 0).toFixed(2)
|
||||
: row[column.prop] ?? '';
|
||||
});
|
||||
return result;
|
||||
});
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(rows), '结算明细');
|
||||
XLSX.writeFile(
|
||||
workbook,
|
||||
`${this.form.formalSettlementNo || '正式结算单'}结算明细${this.$dayjs().format(
|
||||
'YYYY-MM-DD HH-mm-ss'
|
||||
)}.xlsx`
|
||||
);
|
||||
},
|
||||
async openAdjustDialog(row) {
|
||||
this.adjust = {
|
||||
@@ -664,7 +1177,15 @@ export default {
|
||||
async handleSave() {
|
||||
await this.$refs.formRef.validate();
|
||||
if (!this.form.sourcePreSettlementIds.length && !this.form.sourceDetailIds.length) {
|
||||
return this.$message.warning('请选择预结算单或结算明细');
|
||||
return this.$message.warning('请选择结算明细');
|
||||
}
|
||||
const invalidManualFee = this.summaryFees.find(
|
||||
row =>
|
||||
row.manualFlag === 1 &&
|
||||
(!String(row.feeType || '').trim() || !String(row.feeItem || '').trim())
|
||||
);
|
||||
if (invalidManualFee) {
|
||||
return this.$message.warning('请完善手工费用的费用类型和费用项');
|
||||
}
|
||||
this.saving = true;
|
||||
try {
|
||||
@@ -676,6 +1197,22 @@ export default {
|
||||
sourceDetailIds: this.form.sourceDetailIds,
|
||||
exchangeRateDate: this.form.exchangeRateDate,
|
||||
exchangeRate: this.form.exchangeRate,
|
||||
summaryFees: this.summaryFees
|
||||
.filter(
|
||||
row =>
|
||||
row.manualFlag === 1 ||
|
||||
row.id ||
|
||||
Number(row.adjustAmount || 0) !== 0 ||
|
||||
String(row.remark || '').trim()
|
||||
)
|
||||
.map(row => ({
|
||||
id: row.id || undefined,
|
||||
feeType: row.feeType,
|
||||
feeItem: row.feeItem,
|
||||
adjustAmount: Number(row.adjustAmount || 0),
|
||||
remark: row.remark,
|
||||
manualFlag: row.manualFlag || 0,
|
||||
})),
|
||||
attachmentsJson: JSON.stringify(this.attachments || []),
|
||||
remark: this.form.remark,
|
||||
});
|
||||
@@ -722,6 +1259,27 @@ export default {
|
||||
<style scoped lang="scss">
|
||||
.formal-editor__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-editor__detail-filter {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px 16px;
|
||||
margin-bottom: 8px;
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.formal-editor__detail-filter-actions {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: start;
|
||||
}
|
||||
.formal-editor__page-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 0 4px;
|
||||
border-top: 1px solid #eff1f7;
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-editor__links {
|
||||
@@ -756,4 +1314,10 @@ export default {
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.formal-editor__detail-filter {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<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">
|
||||
@@ -20,7 +16,7 @@
|
||||
class="pre-settlement-editor__form"
|
||||
>
|
||||
<el-row :gutter="24">
|
||||
<el-col v-for="field in formFields" :key="field.prop" :span="field.span || 6">
|
||||
<el-col v-for="field in formFields" :key="field.prop" :span="field.span || 8">
|
||||
<el-form-item :label="field.label" :prop="field.prop">
|
||||
<el-select
|
||||
v-if="field.type === 'contract'"
|
||||
@@ -54,7 +50,6 @@
|
||||
<el-input-number
|
||||
v-else-if="field.type === 'number'"
|
||||
v-model="form[field.prop]"
|
||||
:class="field.class"
|
||||
:min="0"
|
||||
:precision="6"
|
||||
:controls="false"
|
||||
@@ -131,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)"
|
||||
@@ -232,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"
|
||||
@@ -254,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>
|
||||
@@ -266,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>
|
||||
@@ -302,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>
|
||||
@@ -328,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">
|
||||
@@ -389,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
|
||||
@@ -401,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"
|
||||
@@ -560,17 +573,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" />
|
||||
@@ -646,6 +648,14 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
pageMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
initialData: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'success'],
|
||||
data() {
|
||||
@@ -665,6 +675,7 @@ export default {
|
||||
contractOptions: [],
|
||||
feeOptions: [],
|
||||
feeCategoryOptions: [],
|
||||
transportTypeOptions: [],
|
||||
summaryFees: [],
|
||||
details: [],
|
||||
detailCollapsed: false,
|
||||
@@ -737,6 +748,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);
|
||||
},
|
||||
@@ -757,7 +787,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();
|
||||
@@ -785,9 +836,12 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
modelValue: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
if (value) this.initialize();
|
||||
},
|
||||
},
|
||||
summaryTotal(value) {
|
||||
this.form.settlementAmount = Number(value || 0).toFixed(2);
|
||||
this.recalculateLocalAmount();
|
||||
@@ -802,7 +856,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();
|
||||
@@ -871,6 +971,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) {
|
||||
@@ -907,6 +1011,15 @@ export default {
|
||||
this.$message.warning('请至少选择一条结算明细');
|
||||
return;
|
||||
}
|
||||
const invalidManualFeeIndex = this.summaryFees.findIndex(
|
||||
row =>
|
||||
Number(row.manualFlag) === 1 &&
|
||||
(!String(row.feeType || '').trim() || !String(row.feeItem || '').trim())
|
||||
);
|
||||
if (invalidManualFeeIndex >= 0) {
|
||||
this.$message.warning(`请完善结算合计第${invalidManualFeeIndex + 1}行的费用类型和费用项`);
|
||||
return;
|
||||
}
|
||||
const stateKey = shouldSubmit ? 'submitting' : 'saving';
|
||||
this[stateKey] = true;
|
||||
try {
|
||||
@@ -916,12 +1029,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;
|
||||
}
|
||||
@@ -937,7 +1050,7 @@ export default {
|
||||
sourceDetailIds: this.details.map(row => row.sourceDetailId || row.id),
|
||||
summaryFees: this.summaryFees.map(row => ({
|
||||
id: row.id || undefined,
|
||||
feeType: row.feeType,
|
||||
feeType: row.feeType || '',
|
||||
feeItem: row.feeItem,
|
||||
adjustAmount: Number(row.adjustAmount || 0),
|
||||
remark: row.remark,
|
||||
@@ -964,12 +1077,23 @@ export default {
|
||||
return this.feeOptions.find(item => item.feeType === feeType)?.feeItems || [];
|
||||
},
|
||||
feeCategoryName(value) {
|
||||
if (value === undefined || value === null || value === '') return '-';
|
||||
if (value === undefined || value === null || value === '') return '';
|
||||
const option = this.feeCategoryOptions.find(
|
||||
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
|
||||
);
|
||||
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);
|
||||
},
|
||||
@@ -981,6 +1105,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);
|
||||
@@ -1056,12 +1249,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 };
|
||||
@@ -1140,7 +1334,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;
|
||||
}
|
||||
@@ -1315,16 +1509,25 @@ 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),
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.exchange-rate-input .el-input__inner) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
&__section-actions,
|
||||
|
||||
@@ -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>
|
||||
@@ -46,41 +46,149 @@
|
||||
v-model="activeSettlementType"
|
||||
@tab-change="handleSettlementTypeChange"
|
||||
>
|
||||
<el-tab-pane label="应付" name="payable">
|
||||
<formal-settlement-table-panel
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:page="page"
|
||||
:has-permission="hasPermission"
|
||||
@create="openCreate"
|
||||
@payment="openPaymentDialog"
|
||||
@sync="handleSync"
|
||||
@export="handleExport"
|
||||
@refresh="loadTable"
|
||||
@page-change="handlePageChange"
|
||||
@update:selection="selection = $event"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="应收" name="receivable">
|
||||
<formal-settlement-table-panel
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:page="page"
|
||||
:has-permission="hasPermission"
|
||||
@create="openCreate"
|
||||
@payment="openPaymentDialog"
|
||||
@sync="handleSync"
|
||||
@export="handleExport"
|
||||
@refresh="loadTable"
|
||||
@page-change="handlePageChange"
|
||||
@update:selection="selection = $event"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="应付" name="payable" />
|
||||
<el-tab-pane label="应收" name="receivable" />
|
||||
</el-tabs>
|
||||
<div class="formal-page__toolbar">
|
||||
<div>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_add')"
|
||||
type="primary"
|
||||
@click="openCreate"
|
||||
>新增</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_payment')"
|
||||
type="primary"
|
||||
plain
|
||||
disabled
|
||||
@click="openPaymentDialog"
|
||||
>付款申请</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_sync')"
|
||||
type="primary"
|
||||
plain
|
||||
disabled
|
||||
@click="handleSync"
|
||||
>同步金蝶</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_print')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handlePrint"
|
||||
>打印结算单</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="hasPermission('formal_settlement_export')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleExport"
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="formal-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border @selection-change="selection = $event">
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
:key="column.prop"
|
||||
v-bind="column"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="column.link && row[column.prop]" type="primary" @click="openView(row)">{{
|
||||
row[column.prop]
|
||||
}}</el-link>
|
||||
<el-tag v-else-if="column.status" :type="statusType(row.approvalStatus)">{{
|
||||
displayValue(row[column.prop])
|
||||
}}</el-tag>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop], row.currency) }}</span>
|
||||
<span v-else-if="column.prop === 'invoiceStatusName'">{{
|
||||
invoiceName(row.invoiceStatus)
|
||||
}}</span>
|
||||
<span v-else-if="column.prop === 'paymentStatusName'">{{
|
||||
paymentName(row.paymentStatus)
|
||||
}}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }"
|
||||
><div class="formal-page__links">
|
||||
<el-link
|
||||
v-if="hasPermission('formal_settlement_view')"
|
||||
type="primary"
|
||||
@click="openView(row)"
|
||||
>查看</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('formal_settlement_edit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="openEdit(row)"
|
||||
>编辑</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('formal_settlement_delete') && row.approvalStatus === 'draft'"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('formal_settlement_submit') && isEditable(row)"
|
||||
type="primary"
|
||||
@click="handleSubmit(row)"
|
||||
>提交</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleApprove(row)"
|
||||
>通过</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleReturn(row)"
|
||||
>驳回</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('formal_settlement_void') &&
|
||||
row.approvalStatus === 'approved' &&
|
||||
row.kingdeeSyncStatus !== 'synced'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleVoid(row)"
|
||||
>作废</el-link
|
||||
>
|
||||
</div></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="formal-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadTable"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<formal-settlement-editor
|
||||
v-model="editor.visible"
|
||||
@@ -142,7 +250,6 @@ import {
|
||||
} from '@/option/settlement/formalSettlementSearch';
|
||||
import { formalSettlementTableColumns } from '@/option/settlement/formalSettlementTable';
|
||||
import FormalSettlementEditor from './components/formal-settlement-editor.vue';
|
||||
import FormalSettlementTablePanel from './components/formal-settlement-table-panel.vue';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
formalSettlementNo: '',
|
||||
@@ -161,7 +268,7 @@ const emptyQuery = () => ({
|
||||
|
||||
export default {
|
||||
name: 'FormalSettlement',
|
||||
components: { FormalSettlementEditor, FormalSettlementTablePanel },
|
||||
components: { FormalSettlementEditor },
|
||||
data() {
|
||||
return {
|
||||
ArrowDown,
|
||||
@@ -228,30 +335,21 @@ export default {
|
||||
toggleSearch() {
|
||||
this.searchExpanded = !this.searchExpanded;
|
||||
},
|
||||
handlePageChange({ current, size }) {
|
||||
this.page.current = current;
|
||||
this.page.size = size;
|
||||
handleSizeChange() {
|
||||
this.page.current = 1;
|
||||
this.loadTable();
|
||||
},
|
||||
handleAction({ type, row }) {
|
||||
const map = {
|
||||
view: this.openView,
|
||||
edit: this.openEdit,
|
||||
delete: this.handleDelete,
|
||||
submit: this.handleSubmit,
|
||||
approve: this.handleApprove,
|
||||
return: this.handleReturn,
|
||||
void: this.handleVoid,
|
||||
print: this.handlePrint,
|
||||
};
|
||||
const fn = map[type];
|
||||
if (fn) fn(row);
|
||||
},
|
||||
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 };
|
||||
@@ -338,7 +436,8 @@ export default {
|
||||
this.paymentDialog.loading = false;
|
||||
}
|
||||
},
|
||||
handlePrint(row) {
|
||||
handlePrint() {
|
||||
const row = this.selectedOne('打印');
|
||||
if (!row) return;
|
||||
const win = window.open('', '_blank');
|
||||
if (!win) return this.$message.warning('浏览器阻止了打印窗口,请允许弹窗后重试');
|
||||
@@ -479,6 +578,28 @@ export default {
|
||||
.formal-page__table-panel {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.formal-page__toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.formal-page__toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-page__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.formal-page__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.formal-page :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -62,7 +62,7 @@
|
||||
v-if="hasPermission('pre_settlement_formal')"
|
||||
type="primary"
|
||||
plain
|
||||
disabled
|
||||
:disabled="!selection.length"
|
||||
@click="handleFormalSettlement"
|
||||
>
|
||||
尾款结算
|
||||
@@ -315,7 +315,6 @@ import {
|
||||
applyAdvance,
|
||||
approve,
|
||||
exportList,
|
||||
formalSettlement,
|
||||
getDetail,
|
||||
getList,
|
||||
getPrintTemplates,
|
||||
@@ -325,6 +324,7 @@ import {
|
||||
} from '@/api/settlement/preSettlement';
|
||||
import { preSettlementSearchFields } from '@/option/settlement/preSettlementSearch';
|
||||
import { preSettlementTableColumns } from '@/option/settlement/preSettlementTable';
|
||||
import { createSettlementTransfer } from '@/utils/settlement-transfer';
|
||||
import { downloadFile } from '@/utils/util';
|
||||
import PreSettlementEditor from './components/pre-settlement-editor.vue';
|
||||
|
||||
@@ -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 };
|
||||
@@ -529,23 +535,37 @@ export default {
|
||||
this.advanceDialog.submitting = false;
|
||||
}
|
||||
},
|
||||
async handleFormalSettlement() {
|
||||
const row = this.selectedOne('尾款结算');
|
||||
if (!row) return;
|
||||
if (
|
||||
row.approvalStatus !== 'approved' ||
|
||||
row.settlementType !== 'payable' ||
|
||||
row.formalSettlementNo
|
||||
) {
|
||||
this.$message.warning('仅审批通过、未转正式结算的应付预结算单可发起尾款结算');
|
||||
handleFormalSettlement() {
|
||||
if (!this.selection.length) {
|
||||
this.$message.warning('请选择需要转正式结算的预结算单');
|
||||
return;
|
||||
}
|
||||
await this.$confirm('转正式结算后无法发起预付,确认继续?', '提示', {
|
||||
type: 'warning',
|
||||
if (this.selection.some(row => row.approvalStatus !== 'approved')) {
|
||||
this.$message.warning('仅审核通过的预结算单可转正式结算');
|
||||
return;
|
||||
}
|
||||
const hasCompleteProjectIds = this.selection.every(
|
||||
row => row.projectId !== undefined && row.projectId !== null && row.projectId !== ''
|
||||
);
|
||||
const projectKeys = new Set(
|
||||
this.selection.map(row =>
|
||||
hasCompleteProjectIds ? String(row.projectId) : String(row.projectName || '')
|
||||
)
|
||||
);
|
||||
if (projectKeys.size > 1) {
|
||||
this.$message.warning('尾款结算只能选择同一项目下的预结算单');
|
||||
return;
|
||||
}
|
||||
const transferToken = createSettlementTransfer({
|
||||
sourcePreSettlements: this.selection.map(row => ({
|
||||
...row,
|
||||
preSettlementId: row.preSettlementId || row.id,
|
||||
})),
|
||||
});
|
||||
this.$router.push({
|
||||
path: '/settlement/formal-settlement/form',
|
||||
query: { mode: 'add', name: '新增正式结算', transferToken },
|
||||
});
|
||||
const { data } = await formalSettlement(row.id);
|
||||
this.$message.success(`正式结算单已生成:${data?.data || ''}`);
|
||||
this.loadTable();
|
||||
},
|
||||
async openPrintDialog() {
|
||||
const row = this.selectedOne('打印结算单');
|
||||
|
||||
@@ -80,10 +80,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>
|
||||
@@ -113,12 +121,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">
|
||||
@@ -181,12 +184,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
|
||||
@@ -321,7 +319,7 @@
|
||||
<el-form-item label="转结算类型">
|
||||
<el-radio-group
|
||||
v-model="transferForm.settlementBillType"
|
||||
@change="loadTransferCandidates"
|
||||
@change="handleTransferTypeChange"
|
||||
>
|
||||
<el-radio label="pre">预结算单</el-radio>
|
||||
<el-radio label="formal">正式结算单</el-radio>
|
||||
@@ -346,19 +344,26 @@
|
||||
<el-input v-else v-model="transferQuery[field.prop]" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadTransferCandidates">查询</el-button>
|
||||
<el-button type="primary" @click="confirmTransferSelection">确认选择</el-button>
|
||||
<el-button type="primary" @click="handleTransferSearch">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table
|
||||
ref="transferTableRef"
|
||||
v-loading="transferDialog.loading"
|
||||
:data="transferRows"
|
||||
row-key="id"
|
||||
border
|
||||
@selection-change="transferSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="52" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column type="selection" width="52" align="center" reserve-selection />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="70"
|
||||
align="center"
|
||||
:index="transferIndexMethod"
|
||||
/>
|
||||
<el-table-column
|
||||
v-for="column in tableColumns.slice(0, 13)"
|
||||
:key="column.prop"
|
||||
@@ -371,6 +376,17 @@
|
||||
<template #default="{ row }">{{ formatColumnValue(row, column) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="settlement-detail-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="transferPage.current"
|
||||
v-model:page-size="transferPage.size"
|
||||
:total="transferPage.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadTransferCandidates"
|
||||
@size-change="handleTransferSizeChange"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="transferDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="transferDialog.submitting" @click="submitTransfer">
|
||||
@@ -478,11 +494,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>
|
||||
@@ -514,7 +526,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>
|
||||
@@ -538,9 +550,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: {
|
||||
@@ -568,7 +582,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 },
|
||||
@@ -593,6 +613,7 @@ export default {
|
||||
transferForm: { settlementBillType: 'formal' },
|
||||
transferRows: [],
|
||||
transferSelection: [],
|
||||
transferPage: { current: 1, size: 10, total: 0 },
|
||||
generateDialog: { visible: false, loading: false },
|
||||
generateQuery: {},
|
||||
generateRows: [],
|
||||
@@ -665,7 +686,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);
|
||||
},
|
||||
@@ -674,7 +698,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);
|
||||
@@ -686,8 +715,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, {
|
||||
@@ -747,7 +775,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) {
|
||||
@@ -779,13 +813,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);
|
||||
@@ -871,7 +901,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) {
|
||||
@@ -883,7 +915,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('请完整填写手工费用项目和金额');
|
||||
@@ -1017,11 +1050,12 @@ export default {
|
||||
this.transferQuery = {};
|
||||
this.transferRows = [];
|
||||
this.transferSelection = [];
|
||||
this.transferPage = { current: 1, size: 10, total: 0 };
|
||||
this.$nextTick(() => this.$refs.transferTableRef?.clearSelection());
|
||||
this.loadTransferCandidates();
|
||||
},
|
||||
async loadTransferCandidates() {
|
||||
this.transferDialog.loading = true;
|
||||
this.transferSelection = [];
|
||||
try {
|
||||
const params = this.buildRequestParams(
|
||||
this.normalizeQuery(
|
||||
@@ -1031,27 +1065,55 @@ export default {
|
||||
'generateEndDate'
|
||||
)
|
||||
);
|
||||
const res = await api.getTransferCandidates(1, 50, {
|
||||
const res = await api.getTransferCandidates(
|
||||
this.transferPage.current,
|
||||
this.transferPage.size,
|
||||
{
|
||||
...params,
|
||||
settlementStatus: 'pending',
|
||||
settlementType: this.settlementType || undefined,
|
||||
settlementBillType: this.transferForm.settlementBillType,
|
||||
});
|
||||
}
|
||||
);
|
||||
const data = this.unwrapPage(res);
|
||||
this.transferRows = (data.records || [])
|
||||
.filter(row => this.isTransferCandidate(row))
|
||||
.map(this.decorateRow);
|
||||
this.transferPage.total = data.total || 0;
|
||||
} finally {
|
||||
this.transferDialog.loading = false;
|
||||
}
|
||||
},
|
||||
resetTransferSelection() {
|
||||
this.transferSelection = [];
|
||||
this.$nextTick(() => this.$refs.transferTableRef?.clearSelection());
|
||||
},
|
||||
handleTransferSearch() {
|
||||
this.transferPage.current = 1;
|
||||
this.resetTransferSelection();
|
||||
this.loadTransferCandidates();
|
||||
},
|
||||
handleTransferTypeChange() {
|
||||
this.transferPage.current = 1;
|
||||
this.resetTransferSelection();
|
||||
this.loadTransferCandidates();
|
||||
},
|
||||
handleTransferSizeChange() {
|
||||
this.transferPage.current = 1;
|
||||
this.loadTransferCandidates();
|
||||
},
|
||||
transferIndexMethod(index) {
|
||||
return (this.transferPage.current - 1) * this.transferPage.size + index + 1;
|
||||
},
|
||||
isTransferCandidate(row) {
|
||||
const hasValue = value => {
|
||||
if (Array.isArray(value)) return value.length > 0;
|
||||
return value !== null && value !== undefined && String(value).trim() !== '';
|
||||
};
|
||||
const normalizeSettlementType = value => {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
const normalized = String(value || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (normalized === '应收') return 'receivable';
|
||||
if (normalized === '应付') return 'payable';
|
||||
return normalized;
|
||||
@@ -1062,12 +1124,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,
|
||||
@@ -1088,13 +1151,6 @@ export default {
|
||||
!hasFormalSettlement
|
||||
);
|
||||
},
|
||||
confirmTransferSelection() {
|
||||
if (!this.transferSelection.length) {
|
||||
this.$message.warning('请选择需要转结算的明细');
|
||||
return;
|
||||
}
|
||||
this.$message.success(`已选择${this.transferSelection.length}条明细`);
|
||||
},
|
||||
transferSelectionChange(selection) {
|
||||
this.transferSelection = selection;
|
||||
},
|
||||
@@ -1103,20 +1159,79 @@ 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 && item.projectId)) return rows;
|
||||
const contractId = rows.find(item => item.contractId)?.contractId;
|
||||
const contractNo = rows.find(item => item.contractNo)?.contractNo;
|
||||
const { data } = await getSettlementContractOptions(contractNo);
|
||||
const contracts = data?.data || [];
|
||||
const contract = contracts.find(
|
||||
item =>
|
||||
(contractId && String(item.id) === String(contractId)) ||
|
||||
(contractNo && String(item.contractNo) === String(contractNo))
|
||||
);
|
||||
if (!contract?.id || !contract.projectId) {
|
||||
this.$message.warning('未找到所选明细对应的合同或项目信息,无法转结算');
|
||||
return null;
|
||||
}
|
||||
return rows.map(item => {
|
||||
const settlementType =
|
||||
item.settlementType || this.settlementType || contract.settlementType;
|
||||
return {
|
||||
...item,
|
||||
contractId: item.contractId || contract.id,
|
||||
contractNo: item.contractNo || contract.contractNo,
|
||||
contractName: item.contractName || contract.contractName,
|
||||
projectId: item.projectId || contract.projectId,
|
||||
projectName: item.projectName || contract.projectName,
|
||||
deptId: item.deptId || contract.deptId,
|
||||
deptName: item.deptName || contract.deptName,
|
||||
payerName:
|
||||
item.payerName || (settlementType === 'receivable' ? contract.partyB : contract.partyA),
|
||||
payeeName:
|
||||
item.payeeName || (settlementType === 'receivable' ? contract.partyA : contract.partyB),
|
||||
};
|
||||
});
|
||||
},
|
||||
openGenerateDialog() {
|
||||
this.generateDialog.visible = true;
|
||||
this.generateQuery = {};
|
||||
@@ -1128,8 +1243,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) {
|
||||
@@ -1139,9 +1255,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;
|
||||
@@ -1189,12 +1314,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;
|
||||
@@ -1208,6 +1370,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 {
|
||||
@@ -1228,7 +1396,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 => {
|
||||
@@ -1255,7 +1428,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;
|
||||
},
|
||||
|
||||
@@ -1309,6 +1309,11 @@ export default {
|
||||
},
|
||||
normalizeBirthday(value = '') {
|
||||
const birthday = String(value || '').trim();
|
||||
const compactMatch = birthday.match(/^(\d{4})(\d{2})(\d{2})$/);
|
||||
if (compactMatch) {
|
||||
const [, year, month, day] = compactMatch;
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
const match = birthday.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/);
|
||||
if (!match) return birthday;
|
||||
const [, year, month, day] = match;
|
||||
@@ -1343,6 +1348,10 @@ export default {
|
||||
}
|
||||
const row = {
|
||||
...this.driverForm,
|
||||
birthday: this.normalizeBirthday(this.driverForm.birthday),
|
||||
drivingLicenseStartDate: this.normalizeBirthday(this.driverForm.drivingLicenseStartDate),
|
||||
drivingLicenseEndDate: this.normalizeBirthday(this.driverForm.drivingLicenseEndDate),
|
||||
qualificationEndDate: this.normalizeBirthday(this.driverForm.qualificationEndDate),
|
||||
qualificationNo: this.driverForm.qualificationNo || this.driverForm.idCardNo,
|
||||
posts: this.driverForm.postList.join(','),
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
placeholder="请输入"
|
||||
@input="value => normalizeDecimalInput('directEconomicLoss', value)"
|
||||
>
|
||||
<template #suffix>元</template>
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</template>
|
||||
<template #insuranceClaimAmountForm>
|
||||
@@ -89,7 +89,7 @@
|
||||
placeholder="请输入"
|
||||
@input="value => normalizeDecimalInput('insuranceClaimAmount', value)"
|
||||
>
|
||||
<template #suffix>元</template>
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</template>
|
||||
<template #attachments="{ row }">
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
placeholder="请输入"
|
||||
@input="value => normalizeDecimalInput('fee', value)"
|
||||
>
|
||||
<template #suffix>元</template>
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</template>
|
||||
<template #attachments="{ row }">
|
||||
|
||||
@@ -129,7 +129,13 @@
|
||||
|
||||
<section-card title="评估标准">
|
||||
<template #extra>
|
||||
<el-button type="primary" plain icon="el-icon-plus" v-if="!readonly" @click="openStandard()">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
v-if="!readonly"
|
||||
@click="openStandard()"
|
||||
>
|
||||
添加评估标准
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -166,7 +172,13 @@
|
||||
<div class="category-line">评分类目:{{ currentCategory.categoryName }}</div>
|
||||
<section-card title="评分项目">
|
||||
<template #extra>
|
||||
<el-button type="primary" plain icon="el-icon-plus" v-if="!readonly" @click="openItem()">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
v-if="!readonly"
|
||||
@click="openItem()"
|
||||
>
|
||||
添加评分项目
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -218,23 +230,93 @@
|
||||
<el-input v-model="itemForm.itemName" maxlength="20" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分值" required>
|
||||
<el-input-number v-model="itemForm.score" :min="1" :precision="0" :step="1" :controls="false" />
|
||||
<el-input-number
|
||||
v-model="itemForm.score"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:controls="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="得分说明">
|
||||
<el-input v-model="itemForm.scoreDescription" maxlength="300" class="score-description-input" />
|
||||
<el-input
|
||||
v-model="itemForm.scoreDescription"
|
||||
maxlength="300"
|
||||
class="score-description-input"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="选项描述">
|
||||
<el-input v-model="itemForm.optionDescription" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="选项" required>
|
||||
<el-form-item label="选项类型" required>
|
||||
<el-radio-group v-model="itemForm.optionType" @change="handleOptionTypeChange">
|
||||
<el-radio value="option">选项</el-radio>
|
||||
<el-radio value="score">分值</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="itemForm.optionType === 'score'" label="基准数值" required>
|
||||
<el-input
|
||||
:model-value="itemForm.baseValue"
|
||||
inputmode="decimal"
|
||||
@input="handleBaseValueInput"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="itemForm.optionType === 'score' ? '阶梯配置' : '选项'" required>
|
||||
<div class="option-editor">
|
||||
<div class="option-row" v-for="(option, index) in itemForm.options" :key="index">
|
||||
<div
|
||||
class="option-row"
|
||||
:class="{ 'option-row--score': itemForm.optionType === 'score' }"
|
||||
v-for="(option, index) in itemForm.options"
|
||||
:key="index"
|
||||
>
|
||||
<template v-if="itemForm.optionType === 'score'">
|
||||
<el-select v-model="option.changeType" class="option-change-type">
|
||||
<el-option label="每增加" value="increase" />
|
||||
<el-option label="每减少" value="decrease" />
|
||||
</el-select>
|
||||
<el-input
|
||||
:model-value="option.changeValue"
|
||||
inputmode="decimal"
|
||||
@input="value => handleOptionDecimalInput(option, 'changeValue', value)"
|
||||
>
|
||||
<template #append>
|
||||
<el-select v-model="option.changeUnit" class="option-unit-select">
|
||||
<el-option
|
||||
v-for="unit in scoreUnitOptions"
|
||||
:key="unit"
|
||||
:label="unit"
|
||||
:value="unit"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select v-model="option.scoreType" class="option-score-type">
|
||||
<el-option label="加" value="add" />
|
||||
<el-option label="减" value="subtract" />
|
||||
</el-select>
|
||||
<div class="option-score">
|
||||
<el-input
|
||||
:model-value="option.score"
|
||||
inputmode="decimal"
|
||||
@input="value => handleOptionDecimalInput(option, 'score', value)"
|
||||
>
|
||||
<template #append>分</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-input v-model="option.optionName" maxlength="100" />
|
||||
<div class="option-score">
|
||||
<el-input-number v-model="option.score" :min="1" :precision="0" :step="1" :controls="false" />
|
||||
<span class="score-unit">分</span>
|
||||
<el-input
|
||||
:model-value="option.score"
|
||||
inputmode="decimal"
|
||||
@input="value => handleOptionDecimalInput(option, 'score', value)"
|
||||
>
|
||||
<template #append>分</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</template>
|
||||
<el-link
|
||||
type="danger"
|
||||
class="option-delete"
|
||||
@@ -244,10 +326,8 @@
|
||||
删除
|
||||
</el-link>
|
||||
</div>
|
||||
<div class="option-add">
|
||||
<el-link type="primary" v-if="!readonly" @click="addOption">
|
||||
+添加选项
|
||||
</el-link>
|
||||
<div class="option-add" v-if="itemForm.optionType !== 'score'">
|
||||
<el-link type="primary" v-if="!readonly" @click="addOption"> +添加选项 </el-link>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -267,7 +347,13 @@
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<section-card title="评估标准设置">
|
||||
<el-form :model="standardForm" label-position="right" label-width="auto" :disabled="readonly" class="standard-form archive-form">
|
||||
<el-form
|
||||
:model="standardForm"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
:disabled="readonly"
|
||||
class="standard-form archive-form"
|
||||
>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="信用等级" required>
|
||||
@@ -413,6 +499,7 @@ export default {
|
||||
label: item,
|
||||
value: item,
|
||||
})),
|
||||
scoreUnitOptions: ['%', '件', '次', '项', '天'],
|
||||
option: {
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
@@ -536,7 +623,19 @@ export default {
|
||||
score: 1,
|
||||
scoreDescription: '',
|
||||
optionDescription: '',
|
||||
options: [{ optionName: '', score: 1 }],
|
||||
optionType: 'option',
|
||||
baseValue: '',
|
||||
options: [this.emptyOption()],
|
||||
};
|
||||
},
|
||||
emptyOption() {
|
||||
return {
|
||||
optionName: '',
|
||||
score: 1,
|
||||
changeType: 'increase',
|
||||
changeValue: '',
|
||||
changeUnit: '%',
|
||||
scoreType: 'add',
|
||||
};
|
||||
},
|
||||
emptyStandard() {
|
||||
@@ -559,14 +658,20 @@ export default {
|
||||
...category,
|
||||
items: (category.items || []).map(item => ({
|
||||
...item,
|
||||
optionType: this.normalizeOptionType(item.optionType),
|
||||
options:
|
||||
item.options && item.options.length ? item.options : [{ optionName: '', score: 1 }],
|
||||
item.options && item.options.length
|
||||
? item.options.map(option => this.normalizeOption(option))
|
||||
: [this.emptyOption()],
|
||||
})),
|
||||
};
|
||||
});
|
||||
return {
|
||||
...detail,
|
||||
categories: categories.map(category => map[category.categoryCode] || category),
|
||||
categories: categories.map(category => ({
|
||||
...(map[category.categoryCode] || category),
|
||||
categoryName: category.categoryName,
|
||||
})),
|
||||
standards: detail.standards || [],
|
||||
};
|
||||
},
|
||||
@@ -596,10 +701,84 @@ export default {
|
||||
openItem(row, index = -1) {
|
||||
this.currentItemIndex = index;
|
||||
this.itemForm = row
|
||||
? { ...this.emptyItem(), ...JSON.parse(JSON.stringify(row)) }
|
||||
? this.normalizeItem({ ...this.emptyItem(), ...JSON.parse(JSON.stringify(row)) })
|
||||
: this.emptyItem();
|
||||
this.itemBox = true;
|
||||
},
|
||||
normalizeOptionType(value) {
|
||||
return value === 'score' || value === 2 || String(value) === 'score' ? 'score' : 'option';
|
||||
},
|
||||
normalizeItem(item = {}) {
|
||||
return {
|
||||
...this.emptyItem(),
|
||||
...item,
|
||||
optionType: this.normalizeOptionType(item.optionType),
|
||||
options:
|
||||
item.options && item.options.length
|
||||
? item.options.map(option => this.normalizeOption(option))
|
||||
: [this.emptyOption()],
|
||||
};
|
||||
},
|
||||
normalizeOption(option = {}) {
|
||||
const normalized = { ...this.emptyOption(), ...option };
|
||||
return {
|
||||
...normalized,
|
||||
changeType:
|
||||
normalized.changeType === 'decrease' ||
|
||||
normalized.changeType === '每减少' ||
|
||||
normalized.changeType === 2
|
||||
? 'decrease'
|
||||
: 'increase',
|
||||
scoreType:
|
||||
normalized.scoreType === 'subtract' ||
|
||||
normalized.scoreType === '减' ||
|
||||
normalized.scoreType === 2
|
||||
? 'subtract'
|
||||
: 'add',
|
||||
};
|
||||
},
|
||||
handleOptionTypeChange(value) {
|
||||
this.itemForm.optionType = this.normalizeOptionType(value);
|
||||
this.itemForm.options = (this.itemForm.options || []).map(option =>
|
||||
this.normalizeOption(option)
|
||||
);
|
||||
},
|
||||
handleBaseValueInput(value) {
|
||||
const source = String(value ?? '').replace(/[^\d.-]/g, '');
|
||||
const negative = source.startsWith('-');
|
||||
const unsigned = source.replace(/-/g, '');
|
||||
const [integer = '', ...decimalParts] = unsigned.split('.');
|
||||
const decimal = decimalParts.join('').slice(0, 10);
|
||||
this.itemForm.baseValue = `${negative ? '-' : ''}${integer}${decimalParts.length ? `.${decimal}` : ''}`;
|
||||
},
|
||||
handleOptionDecimalInput(option, field, value) {
|
||||
option[field] = this.normalizeDecimalInput(value);
|
||||
},
|
||||
normalizeDecimalInput(value) {
|
||||
const source = String(value ?? '').replace(/[^\d.]/g, '');
|
||||
const [integer = '', ...decimalParts] = source.split('.');
|
||||
const decimal = decimalParts.join('').slice(0, 2);
|
||||
return decimalParts.length ? `${integer}.${decimal}` : integer;
|
||||
},
|
||||
toScoreValue(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number.toFixed(2) : value;
|
||||
},
|
||||
prepareSubmitConfig(config) {
|
||||
const payload = JSON.parse(JSON.stringify(config || {}));
|
||||
(payload.categories || []).forEach(category => {
|
||||
(category.items || []).forEach(item => {
|
||||
item.score = this.toScoreValue(item.score);
|
||||
(item.options || []).forEach(option => {
|
||||
option.score = this.toScoreValue(option.score);
|
||||
if (this.normalizeOptionType(item.optionType) === 'score') {
|
||||
option.changeValue = this.toScoreValue(option.changeValue);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return payload;
|
||||
},
|
||||
hasDuplicateItemName(itemName, currentIndex = -1) {
|
||||
const name = String(itemName || '').trim();
|
||||
return this.currentCategory.items.some(
|
||||
@@ -623,14 +802,32 @@ export default {
|
||||
this.$message.warning('请输入分值');
|
||||
return;
|
||||
}
|
||||
if (!this.isPositiveInteger(this.itemForm.score)) {
|
||||
this.$message.warning('分值只能输入正整数');
|
||||
return;
|
||||
}
|
||||
if (!this.itemForm.options.length) {
|
||||
this.$message.warning('至少添加1条档位选项');
|
||||
return;
|
||||
}
|
||||
if (this.itemForm.optionType === 'score') {
|
||||
if (this.itemForm.options.length !== 1) {
|
||||
this.$message.warning('分值类型只能配置1条阶梯配置');
|
||||
return;
|
||||
}
|
||||
if (!this.isAnyNumber(this.itemForm.baseValue)) {
|
||||
this.$message.warning('请输入基准数值');
|
||||
return;
|
||||
}
|
||||
const invalidScoreOption = this.itemForm.options.some(
|
||||
option =>
|
||||
!option.changeType ||
|
||||
!this.isPositiveDecimal(option.changeValue) ||
|
||||
!option.changeUnit ||
|
||||
!option.scoreType ||
|
||||
!this.isPositiveDecimal(option.score)
|
||||
);
|
||||
if (invalidScoreOption) {
|
||||
this.$message.warning('请完整填写基础分值,并确保数值为正数且最多两位小数');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const invalidOption = this.itemForm.options.some(
|
||||
option =>
|
||||
!String(option.optionName || '').trim() ||
|
||||
@@ -642,18 +839,18 @@ export default {
|
||||
this.$message.warning('请完整填写档位选项');
|
||||
return;
|
||||
}
|
||||
if (this.itemForm.options.some(option => !this.isPositiveInteger(option.score))) {
|
||||
this.$message.warning('选项分数只能输入正整数');
|
||||
return;
|
||||
}
|
||||
const item = JSON.parse(JSON.stringify(this.itemForm));
|
||||
item.itemName = String(item.itemName || '').trim();
|
||||
item.optionType = this.normalizeOptionType(item.optionType);
|
||||
item.baseValue = item.optionType === 'score' ? String(item.baseValue).trim() : null;
|
||||
item.options = item.options.map(option => ({
|
||||
...option,
|
||||
optionName: String(option.optionName || '').trim(),
|
||||
score: Number(option.score),
|
||||
score: this.toScoreValue(option.score),
|
||||
...(item.optionType === 'score' ? { changeValue: this.toScoreValue(option.changeValue) } : {}),
|
||||
}));
|
||||
item.score = Number(item.score);
|
||||
item.score = this.toScoreValue(item.score);
|
||||
if (this.currentItemIndex >= 0) {
|
||||
this.currentCategory.items.splice(this.currentItemIndex, 1, item);
|
||||
} else {
|
||||
@@ -669,7 +866,7 @@ export default {
|
||||
this.currentCategory.items.splice(index, 1);
|
||||
},
|
||||
addOption() {
|
||||
this.itemForm.options.push({ optionName: '', score: 1 });
|
||||
this.itemForm.options.push(this.emptyOption());
|
||||
},
|
||||
removeOption(index) {
|
||||
this.itemForm.options.splice(index, 1);
|
||||
@@ -788,8 +985,20 @@ export default {
|
||||
isBlankValue(value) {
|
||||
return value === undefined || value === null || value === '';
|
||||
},
|
||||
isPositiveInteger(value) {
|
||||
return /^\d+$/.test(String(value)) && Number(value) > 0;
|
||||
isPositiveDecimal(value) {
|
||||
const number = Number(value);
|
||||
console.log(
|
||||
value,
|
||||
Number.isFinite(number),
|
||||
number,
|
||||
Math.abs(number * 100 - Math.round(number * 100))
|
||||
);
|
||||
if (!Number.isFinite(number) || number <= 0) return false;
|
||||
return Math.abs(number * 100 - Math.round(number * 100)) < 1e-8;
|
||||
},
|
||||
isAnyNumber(value) {
|
||||
const text = String(value ?? '').trim();
|
||||
return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(text) && Number.isFinite(Number(text));
|
||||
},
|
||||
hasRangeOverlap(list, lowerProp, upperProp) {
|
||||
const ranges = list
|
||||
@@ -822,18 +1031,33 @@ export default {
|
||||
return false;
|
||||
}
|
||||
for (const item of category.items) {
|
||||
if (!this.isPositiveInteger(item.score)) {
|
||||
this.$message.warning(`${item.itemName || '评分项目'}的分值只能为正整数`);
|
||||
return false;
|
||||
}
|
||||
if (!item.options || item.options.length === 0) {
|
||||
this.$message.warning(`${item.itemName}至少存在1条档位选项`);
|
||||
return false;
|
||||
}
|
||||
if (item.options.some(option => !this.isPositiveInteger(option.score))) {
|
||||
this.$message.warning(`${item.itemName || '评分项目'}的选项分数只能为正整数`);
|
||||
if (this.normalizeOptionType(item.optionType) === 'score') {
|
||||
if (item.options.length !== 1) {
|
||||
this.$message.warning(`${item.itemName || '评分项目'}的分值类型只能配置1条阶梯配置`);
|
||||
return false;
|
||||
}
|
||||
if (!this.isAnyNumber(item.baseValue)) {
|
||||
this.$message.warning(`${item.itemName || '评分项目'}的基准数值不能为空且必须为数字`);
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
item.options.some(
|
||||
option =>
|
||||
!option.changeType ||
|
||||
!this.isPositiveDecimal(option.changeValue) ||
|
||||
!this.scoreUnitOptions.includes(option.changeUnit) ||
|
||||
!option.scoreType ||
|
||||
!this.isPositiveDecimal(option.score)
|
||||
)
|
||||
) {
|
||||
this.$message.warning(`${item.itemName || '评分项目'}的基础分值配置不完整`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const creditLevels = this.configForm.standards
|
||||
@@ -862,14 +1086,33 @@ export default {
|
||||
this.$message.warning('发布前请完整填写评分项目名称');
|
||||
return false;
|
||||
}
|
||||
if (!this.isPositiveInteger(item.score)) {
|
||||
this.$message.warning(`${item.itemName || '评分项目'}的分值只能为正整数`);
|
||||
return false;
|
||||
}
|
||||
if (!item.options || item.options.length === 0) {
|
||||
this.$message.warning(`${item.itemName || '评分项目'}至少配置1个档位选项`);
|
||||
return false;
|
||||
}
|
||||
if (this.normalizeOptionType(item.optionType) === 'score') {
|
||||
if (item.options.length !== 1) {
|
||||
this.$message.warning(`${item.itemName || '评分项目'}的分值类型只能配置1条阶梯配置`);
|
||||
return false;
|
||||
}
|
||||
if (!this.isAnyNumber(item.baseValue)) {
|
||||
this.$message.warning(`${item.itemName || '评分项目'}的基准数值不能为空且必须为数字`);
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
item.options.some(
|
||||
option =>
|
||||
!option.changeType ||
|
||||
!this.isPositiveDecimal(option.changeValue) ||
|
||||
!this.scoreUnitOptions.includes(option.changeUnit) ||
|
||||
!option.scoreType ||
|
||||
!this.isPositiveDecimal(option.score)
|
||||
)
|
||||
) {
|
||||
this.$message.warning(`${item.itemName || '评分项目'}的基础分值配置不完整`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const invalidOption = item.options.some(
|
||||
option => !String(option.optionName || '').trim() || this.isBlankValue(option.score)
|
||||
);
|
||||
@@ -877,9 +1120,6 @@ export default {
|
||||
this.$message.warning(`${item.itemName || '评分项目'}的档位选项未填写完整`);
|
||||
return false;
|
||||
}
|
||||
if (item.options.some(option => !this.isPositiveInteger(option.score))) {
|
||||
this.$message.warning(`${item.itemName || '评分项目'}的选项分数只能为正整数`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -938,7 +1178,7 @@ export default {
|
||||
saveConfig() {
|
||||
if (!this.validateConfig()) return;
|
||||
submit({
|
||||
...this.configForm,
|
||||
...this.prepareSubmitConfig(this.configForm),
|
||||
status: 3,
|
||||
}).then(() => {
|
||||
this.configBox = false;
|
||||
@@ -1157,6 +1397,31 @@ export default {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.option-row--score {
|
||||
grid-template-columns: 150px minmax(220px, 1fr) 110px 180px 74px;
|
||||
gap: 16px;
|
||||
|
||||
.option-score {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.option-change-type,
|
||||
.option-score-type {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.option-unit-select {
|
||||
width: 78px;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border: 0;
|
||||
border-left: 1px solid #dcdfe6;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.option-score {
|
||||
position: relative;
|
||||
|
||||
@@ -1278,5 +1543,4 @@ export default {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -637,11 +637,20 @@
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件名称" min-width="180">
|
||||
<el-table-column label="文件名称" min-width="180" align="left">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" :disabled="!row.url" @click="previewQualificationFile(row)">{{
|
||||
row.originalName || row.name || '-'
|
||||
}}</el-link>
|
||||
<div
|
||||
class="qualification-file-name-cell"
|
||||
:title="row.originalName || row.name || '-'"
|
||||
>
|
||||
<span
|
||||
class="qualification-file-name"
|
||||
:class="{ 'is-disabled': !row.url }"
|
||||
@click="row.url && previewQualificationFile(row)"
|
||||
>
|
||||
{{ row.originalName || row.name || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220">
|
||||
@@ -1201,6 +1210,17 @@
|
||||
<el-table-column label="评分标准" min-width="430">
|
||||
<template #default="{ row: detail }">
|
||||
<div class="score-standard-cell">
|
||||
<template v-if="isScoreTypeDetail(detail)">
|
||||
<el-input
|
||||
:model-value="detail.scoreInput"
|
||||
inputmode="decimal"
|
||||
:disabled="readonly"
|
||||
@input="value => handleScoreValueInput(detail, value)"
|
||||
>
|
||||
<template #append>{{ getScoreRule(detail)?.changeUnit || '' }}</template>
|
||||
</el-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="score-standard-cell__label">请选择:</div>
|
||||
<el-select
|
||||
v-model="detail.selectedOption"
|
||||
@@ -1215,6 +1235,7 @@
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -2145,8 +2166,8 @@ export default {
|
||||
const scoreDetails = this.normalizeChangeFieldValue(score.details);
|
||||
const details = (Array.isArray(scoreDetails) ? scoreDetails : [])
|
||||
.map(detail => {
|
||||
const category =
|
||||
detail.categoryName || categoryNames[this.getScoreDetailCategoryCode(detail)] || '';
|
||||
const categoryCode = this.getScoreDetailCategoryCode(detail);
|
||||
const category = categoryNames[categoryCode] || detail.categoryName || '';
|
||||
const itemName = detail.itemName || '评分项目';
|
||||
const option = detail.selectedOption || detail.scoreDescription || '';
|
||||
const points = [
|
||||
@@ -3528,7 +3549,10 @@ export default {
|
||||
quantificationId: score.quantificationId ? String(score.quantificationId) : '',
|
||||
tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit),
|
||||
attachments: score.attachments || this.parseAttachments(score.proofAttachments),
|
||||
details: score.details || [],
|
||||
details: (score.details || []).map(detail => ({
|
||||
...detail,
|
||||
scoreInput: this.normalizeScoreInputValue(detail.scoreInput),
|
||||
})),
|
||||
};
|
||||
},
|
||||
normalizeOptionalAmount(value) {
|
||||
@@ -3827,6 +3851,7 @@ export default {
|
||||
...item,
|
||||
categoryCode: category.categoryCode,
|
||||
categoryName: category.categoryName,
|
||||
scoreInput: this.normalizeScoreInputValue(item.scoreInput),
|
||||
optionsJson:
|
||||
item.optionsJson ||
|
||||
JSON.stringify(
|
||||
@@ -3834,6 +3859,10 @@ export default {
|
||||
label: option.label || option.optionName || option.value,
|
||||
value: option.value || option.optionName || option.label,
|
||||
score: option.score,
|
||||
changeType: option.changeType,
|
||||
changeValue: option.changeValue,
|
||||
changeUnit: option.changeUnit,
|
||||
scoreType: option.scoreType,
|
||||
}))
|
||||
),
|
||||
}))
|
||||
@@ -3853,9 +3882,16 @@ export default {
|
||||
...detail,
|
||||
categoryCode: detail.categoryCode || category?.categoryCode || '',
|
||||
categoryName: detail.categoryName || category?.categoryName || '',
|
||||
scoreInput: this.normalizeScoreInputValue(detail.scoreInput),
|
||||
};
|
||||
});
|
||||
},
|
||||
normalizeScoreInputValue(value) {
|
||||
if (value === undefined || value === null || value === '' || value === -1 || value === '-1') {
|
||||
return '';
|
||||
}
|
||||
return String(value);
|
||||
},
|
||||
getScoreDetailCategoryCode(detail = {}) {
|
||||
const rawCode = String(
|
||||
detail.categoryCode ||
|
||||
@@ -3907,12 +3943,24 @@ export default {
|
||||
parseScoreOptions(value) {
|
||||
if (!value) return [];
|
||||
const normalize = list =>
|
||||
list.map(item => ({
|
||||
list.map(item => {
|
||||
const isScoreOption = item.changeType || item.changeValue || item.changeUnit;
|
||||
const changeLabel = item.changeType === 'decrease' ? '每减少' : '每增加';
|
||||
const scoreLabel = item.scoreType === 'subtract' ? '减' : '加';
|
||||
const generatedLabel = isScoreOption
|
||||
? `${changeLabel}${item.changeValue || ''}${item.changeUnit || ''}${scoreLabel}${item.score || ''}分`
|
||||
: '';
|
||||
const label = item.label || item.optionName || item.value || generatedLabel;
|
||||
return {
|
||||
...item,
|
||||
label: item.label || item.optionName || item.value,
|
||||
value: item.value || item.optionName || item.label,
|
||||
score: item.score,
|
||||
}));
|
||||
label,
|
||||
value: item.value || item.optionName || item.label || generatedLabel,
|
||||
score:
|
||||
item.scoreType === 'subtract' && item.score !== undefined
|
||||
? -Math.abs(Number(item.score))
|
||||
: item.score,
|
||||
};
|
||||
});
|
||||
if (Array.isArray(value)) return normalize(value);
|
||||
try {
|
||||
const options = JSON.parse(value);
|
||||
@@ -3921,6 +3969,50 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
isScoreTypeDetail(detail = {}) {
|
||||
return String(detail.optionType || '').toLowerCase() === 'score';
|
||||
},
|
||||
getScoreRule(detail = {}) {
|
||||
return this.parseScoreOptions(detail.optionsJson)[0] || null;
|
||||
},
|
||||
handleScoreValueInput(detail, value) {
|
||||
const raw = String(value ?? '')
|
||||
.replace(/[^\d.-]/g, '')
|
||||
.replace(/(?!^)-/g, '')
|
||||
.replace(/(\..*)\./g, '$1');
|
||||
const sign = raw.startsWith('-') ? '-' : '';
|
||||
const unsigned = raw.replace(/^-/, '');
|
||||
const hasDecimal = unsigned.includes('.');
|
||||
const [integerPart, decimalPart = ''] = unsigned.split('.');
|
||||
const normalized = `${sign}${integerPart}${hasDecimal ? `.${decimalPart.slice(0, 2)}` : ''}`;
|
||||
detail.scoreInput = normalized;
|
||||
const rule = this.getScoreRule(detail);
|
||||
if (!normalized || normalized === '-' || normalized === '.') {
|
||||
detail.selfScore = '';
|
||||
this.calculateScore(this.currentScore);
|
||||
return;
|
||||
}
|
||||
const base = Number(detail.baseValue);
|
||||
const input = Number(normalized);
|
||||
if (!rule || !Number.isFinite(base) || !Number.isFinite(input)) {
|
||||
detail.selfScore = '';
|
||||
this.calculateScore(this.currentScore);
|
||||
return;
|
||||
}
|
||||
const increase = rule.changeType !== 'decrease';
|
||||
const valid = increase ? input >= base : input <= base;
|
||||
if (!valid) {
|
||||
detail.selfScore = '';
|
||||
this.$message.warning(`输入值${increase ? '不能小于' : '不能大于'}基准数值${base}`);
|
||||
this.calculateScore(this.currentScore);
|
||||
return;
|
||||
}
|
||||
const distance = increase ? input - base : base - input;
|
||||
const stepScore = Math.abs(Number(rule.score || 0));
|
||||
const signedStep = rule.scoreType === 'subtract' ? -stepScore : stepScore;
|
||||
detail.selfScore = Number((Number(detail.score || 0) + distance * signedStep).toFixed(2));
|
||||
this.calculateScore(this.currentScore);
|
||||
},
|
||||
scoreOptionChange(detail, optionValue) {
|
||||
const option = this.parseScoreOptions(detail.optionsJson).find(
|
||||
item => item.value === optionValue
|
||||
@@ -4067,9 +4159,13 @@ export default {
|
||||
return score;
|
||||
},
|
||||
hasIncompleteBasicScoreDetail(details = []) {
|
||||
return this.getScoreCategoryDetailsByList(details, 'basic').some(
|
||||
item => !item.selectedOption
|
||||
);
|
||||
return this.getScoreCategoryDetailsByList(details, 'basic').some(item => {
|
||||
if (this.isScoreTypeDetail(item)) {
|
||||
const input = String(item.scoreInput ?? '').trim();
|
||||
return !input || !Number.isFinite(Number(input)) || item.selfScore === '';
|
||||
}
|
||||
return !item.selectedOption;
|
||||
});
|
||||
},
|
||||
finishScore(score) {
|
||||
if (!score.details || !score.details.length) {
|
||||
@@ -4077,7 +4173,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
if (this.hasIncompleteBasicScoreDetail(score.details)) {
|
||||
this.$message.warning('请完整选择基础得分项评分明细选项');
|
||||
this.$message.warning('请完整填写基础得分项评分明细');
|
||||
return;
|
||||
}
|
||||
this.calculateScore(score);
|
||||
@@ -4139,6 +4235,17 @@ export default {
|
||||
this.$message.warning('当前评分量化表未配置评分项目');
|
||||
return false;
|
||||
}
|
||||
const invalidScoreInput = this.currentScore.details.find(item => {
|
||||
if (!this.isScoreTypeDetail(item)) return false;
|
||||
const input = String(item.scoreInput ?? '').trim();
|
||||
return input !== '' && item.selfScore === '';
|
||||
});
|
||||
if (invalidScoreInput) {
|
||||
this.$message.warning(
|
||||
`${invalidScoreInput.itemName || '评分项目'}的输入值不符合基准数值限制,请重新填写`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const overMaxReviewScore = this.currentScore.details.find(item => {
|
||||
if (
|
||||
item.reviewScore === undefined ||
|
||||
@@ -4160,7 +4267,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
if (requireComplete && this.hasIncompleteBasicScoreDetail(this.currentScore.details)) {
|
||||
this.$message.warning('请完整选择基础得分项评分明细选项');
|
||||
this.$message.warning('请完整填写基础得分项评分明细');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -4462,6 +4569,41 @@ export default {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.qualification-file-name-cell {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.qualification-file-name {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: default;
|
||||
color: #606266;
|
||||
|
||||
&:hover {
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qualification-upload-bar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
|
||||
@@ -70,6 +70,15 @@
|
||||
class="maintenance-plan-page__input"
|
||||
/>
|
||||
</template>
|
||||
<template #address-form>
|
||||
<el-input
|
||||
v-model="form.address"
|
||||
:disabled="boxType === 'view'"
|
||||
readonly
|
||||
placeholder="点击地图选址"
|
||||
@click="openAddressMapPicker"
|
||||
/>
|
||||
</template>
|
||||
<template #mileageForm>
|
||||
<el-input
|
||||
v-model="form.mileage"
|
||||
@@ -140,6 +149,11 @@
|
||||
</template>
|
||||
</avue-form>
|
||||
</el-dialog>
|
||||
<address-map-picker
|
||||
v-model="addressMapPickerVisible"
|
||||
:address="form.address"
|
||||
@confirm="handleAddressMapConfirm"
|
||||
/>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -154,6 +168,7 @@ import { openImportDialog } from '@/utils/import-excel';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
@@ -163,6 +178,9 @@ const createTimeRangeMap = {
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddressMapPicker,
|
||||
},
|
||||
data() {
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
@@ -178,6 +196,7 @@ export default {
|
||||
query: {},
|
||||
loading: true,
|
||||
excelBox: false,
|
||||
addressMapPickerVisible: false,
|
||||
excelForm: {},
|
||||
page: {
|
||||
pageSize: 10,
|
||||
@@ -203,6 +222,7 @@ export default {
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 90,
|
||||
viewBtn: true,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
@@ -315,6 +335,8 @@ export default {
|
||||
{
|
||||
label: '地址',
|
||||
prop: 'address',
|
||||
slot: true,
|
||||
formslot: true,
|
||||
minWidth: 180,
|
||||
overHidden: true,
|
||||
span: 24,
|
||||
@@ -483,6 +505,14 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openAddressMapPicker() {
|
||||
if (this.boxType !== 'view') {
|
||||
this.addressMapPickerVisible = true;
|
||||
}
|
||||
},
|
||||
handleAddressMapConfirm(address) {
|
||||
this.form.address = address;
|
||||
},
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission[code], false);
|
||||
},
|
||||
|
||||
@@ -70,6 +70,15 @@
|
||||
class="maintenance-record-page__input"
|
||||
/>
|
||||
</template>
|
||||
<template #address-form>
|
||||
<el-input
|
||||
v-model="form.address"
|
||||
:disabled="boxType === 'view'"
|
||||
readonly
|
||||
placeholder="点击地图选址"
|
||||
@click="openAddressMapPicker"
|
||||
/>
|
||||
</template>
|
||||
<template #costForm>
|
||||
<el-input
|
||||
v-model="form.cost"
|
||||
@@ -126,6 +135,11 @@
|
||||
</template>
|
||||
</avue-form>
|
||||
</el-dialog>
|
||||
<address-map-picker
|
||||
v-model="addressMapPickerVisible"
|
||||
:address="form.address"
|
||||
@confirm="handleAddressMapConfirm"
|
||||
/>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -140,6 +154,7 @@ import { openImportDialog } from '@/utils/import-excel';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
@@ -149,6 +164,9 @@ const createTimeRangeMap = {
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddressMapPicker,
|
||||
},
|
||||
data() {
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
@@ -164,6 +182,7 @@ export default {
|
||||
query: {},
|
||||
loading: true,
|
||||
excelBox: false,
|
||||
addressMapPickerVisible: false,
|
||||
excelForm: {},
|
||||
isDetailLoading: false,
|
||||
page: {
|
||||
@@ -190,6 +209,7 @@ export default {
|
||||
border: true,
|
||||
index: true,
|
||||
indexLabel: '序号',
|
||||
indexWidth: 90,
|
||||
viewBtn: true,
|
||||
selection: true,
|
||||
dialogClickModal: false,
|
||||
@@ -291,6 +311,8 @@ export default {
|
||||
{
|
||||
label: '地址',
|
||||
prop: 'address',
|
||||
slot: true,
|
||||
formslot: true,
|
||||
minWidth: 180,
|
||||
overHidden: true,
|
||||
span: 24,
|
||||
@@ -478,6 +500,14 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openAddressMapPicker() {
|
||||
if (this.boxType !== 'view') {
|
||||
this.addressMapPickerVisible = true;
|
||||
}
|
||||
},
|
||||
handleAddressMapConfirm(address) {
|
||||
this.form.address = address;
|
||||
},
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission[code], false);
|
||||
},
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
placeholder="请输入"
|
||||
@input="value => normalizeIntegerInput('previousMonthMileage', value)"
|
||||
>
|
||||
<template #suffix>km</template>
|
||||
<template #append>km</template>
|
||||
</el-input>
|
||||
</template>
|
||||
<template #currentMonthMileageForm>
|
||||
@@ -101,7 +101,7 @@
|
||||
placeholder="请输入"
|
||||
@input="value => normalizeIntegerInput('currentMonthMileage', value)"
|
||||
>
|
||||
<template #suffix>km</template>
|
||||
<template #append>km</template>
|
||||
</el-input>
|
||||
</template>
|
||||
<template #monthlyMileageForm>
|
||||
@@ -112,7 +112,7 @@
|
||||
placeholder="请输入"
|
||||
@input="value => normalizeIntegerInput('monthlyMileage', value)"
|
||||
>
|
||||
<template #suffix>km</template>
|
||||
<template #append>km</template>
|
||||
</el-input>
|
||||
</template>
|
||||
<template #totalMileageForm>
|
||||
@@ -123,7 +123,7 @@
|
||||
placeholder="请输入"
|
||||
@input="value => normalizeIntegerInput('totalMileage', value)"
|
||||
>
|
||||
<template #suffix>km</template>
|
||||
<template #append>km</template>
|
||||
</el-input>
|
||||
</template>
|
||||
<template #totalMileageStart-search="{ row }">
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
placeholder="请输入"
|
||||
@input="value => normalizeDecimalInput('unitPrice', value)"
|
||||
>
|
||||
<template #suffix>元</template>
|
||||
<template #append>元/升</template>
|
||||
</el-input>
|
||||
</template>
|
||||
<template #transactionAmountForm>
|
||||
|
||||
@@ -110,6 +110,15 @@
|
||||
@input="value => normalizeIntegerInput('deductPoints', value)"
|
||||
/>
|
||||
</template>
|
||||
<template #location-form>
|
||||
<el-input
|
||||
v-model="form.location"
|
||||
:disabled="boxType === 'view'"
|
||||
readonly
|
||||
placeholder="点击地图选址"
|
||||
@click="openLocationMapPicker"
|
||||
/>
|
||||
</template>
|
||||
<template #processStatus="{ row }">
|
||||
<span :class="row.processStatus === '未处理' ? 'violation-record-page__danger' : ''">
|
||||
{{ row.processStatus }}
|
||||
@@ -152,6 +161,11 @@
|
||||
</template>
|
||||
</avue-form>
|
||||
</el-dialog>
|
||||
<address-map-picker
|
||||
v-model="locationMapPickerVisible"
|
||||
:address="form.location"
|
||||
@confirm="handleLocationMapConfirm"
|
||||
/>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -167,6 +181,7 @@ import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { excelOption, option } from '@/option/vehicle/violation-record';
|
||||
import NProgress from 'nprogress';
|
||||
@@ -177,12 +192,16 @@ const createTimeRangeMap = {
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddressMapPicker,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {},
|
||||
query: {},
|
||||
loading: true,
|
||||
excelBox: false,
|
||||
locationMapPickerVisible: false,
|
||||
excelForm: {},
|
||||
isDetailLoading: false,
|
||||
option,
|
||||
@@ -242,6 +261,14 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openLocationMapPicker() {
|
||||
if (this.boxType !== 'view') {
|
||||
this.locationMapPickerVisible = true;
|
||||
}
|
||||
},
|
||||
handleLocationMapConfirm(address) {
|
||||
this.form.location = address;
|
||||
},
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission[code], false);
|
||||
},
|
||||
|
||||
+13
-13
@@ -50,26 +50,26 @@ export default ({
|
||||
__VUE_I18N_LEGACY_API__: true,
|
||||
__INTLIFY_PROD_DEVTOOLS__: false,
|
||||
},
|
||||
// server: {
|
||||
// port: 2888,
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: 'http://localhost',
|
||||
// //target: 'https://saber3.bladex.cn/api',
|
||||
// changeOrigin: true,
|
||||
// rewrite: path => path.replace(/^\/api/, ''),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
server: {
|
||||
port: 2889,
|
||||
port: 2888,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://172.16.203.228:8000',
|
||||
target: 'http://localhost',
|
||||
//target: 'https://saber3.bladex.cn/api',
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
// server: {
|
||||
// port: 2889,
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: 'http://172.16.203.228:8000',
|
||||
// changeOrigin: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': resolve(__dirname, './'),
|
||||
|
||||
Reference in New Issue
Block a user