478 lines
15 KiB
Vue
478 lines
15 KiB
Vue
<template>
|
||
<basic-container class="payment-page">
|
||
<avue-crud
|
||
ref="crud"
|
||
:option="option"
|
||
:table-loading="loading"
|
||
:data="rows"
|
||
v-model:page="page"
|
||
@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="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="openProjectAdvance"
|
||
>预付申请</el-button
|
||
>
|
||
<el-button
|
||
v-if="hasPermission('payment_application_export')"
|
||
type="primary"
|
||
plain
|
||
@click="handleExport"
|
||
>导出</el-button
|
||
>
|
||
</template>
|
||
|
||
<template #paymentNo="{ row }">
|
||
<el-link type="primary" @click="openView(row)">{{ row.paymentNo }}</el-link>
|
||
</template>
|
||
|
||
<template #approvalStatusName="{ row }">
|
||
<el-tag :type="statusType(row.approvalStatus)" class="status-text">{{
|
||
row.approvalStatusName || '-'
|
||
}}</el-tag>
|
||
</template>
|
||
|
||
<template #kingdeeLockStatus="{ row }">
|
||
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
|
||
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
|
||
</el-tooltip>
|
||
<span v-else>-</span>
|
||
</template>
|
||
|
||
<template #menu="{ row }">
|
||
<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') &&
|
||
['draft', 'returned'].includes(row.approvalStatus)
|
||
"
|
||
type="primary"
|
||
@click="handleSubmit(row)"
|
||
>提交</el-link
|
||
>
|
||
<el-link
|
||
v-if="
|
||
mkEnabled &&
|
||
hasPermission('payment_application_approve') &&
|
||
['reviewing', 'approved'].includes(row.approvalStatus)
|
||
"
|
||
type="primary"
|
||
@click="handleMkAudit(row)"
|
||
>流程</el-link
|
||
>
|
||
<el-link
|
||
v-if="
|
||
!mkEnabled &&
|
||
hasPermission('payment_application_approve') &&
|
||
row.approvalStatus === 'reviewing'
|
||
"
|
||
type="primary"
|
||
@click="handleApprove(row)"
|
||
>通过</el-link
|
||
>
|
||
<el-link
|
||
v-if="
|
||
!mkEnabled &&
|
||
hasPermission('payment_application_approve') &&
|
||
row.approvalStatus === 'reviewing'
|
||
"
|
||
type="danger"
|
||
@click="handleReturn(row)"
|
||
>驳回</el-link
|
||
>
|
||
<el-link
|
||
v-if="
|
||
hasPermission('payment_application_void') &&
|
||
row.approvalStatus === 'approved' &&
|
||
row.kingdeeLockStatus !== 'locked'
|
||
"
|
||
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
|
||
>
|
||
</template>
|
||
</avue-crud>
|
||
</basic-container>
|
||
</template>
|
||
|
||
<script>
|
||
import { mapGetters } from 'vuex';
|
||
import * as api from '@/api/payment/paymentApplication';
|
||
import { paymentApplicationOption } from '@/option/payment/paymentApplication';
|
||
import organizationSearch from '@/mixins/organization-search';
|
||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||
|
||
export default {
|
||
name: 'PaymentApplication',
|
||
mixins: [organizationSearch],
|
||
data() {
|
||
return {
|
||
rows: [],
|
||
selection: [],
|
||
loading: false,
|
||
syncLoading: false,
|
||
resultSyncing: false,
|
||
query: {},
|
||
option: {},
|
||
page: {
|
||
currentPage: 1,
|
||
pageSize: 10,
|
||
pageSizes: [10, 20, 50, 100],
|
||
total: 0,
|
||
},
|
||
mkEnabled: false,
|
||
};
|
||
},
|
||
computed: {
|
||
...mapGetters(['permission']),
|
||
},
|
||
created() {
|
||
this.option = paymentApplicationOption({
|
||
paymentTypeOptions: api.paymentTypeOptions,
|
||
approvalStatusOptions: api.approvalStatusOptions,
|
||
kingdeeStatusOptions: api.kingdeeStatusOptions,
|
||
});
|
||
this.loadOrganizationOptions();
|
||
},
|
||
mounted() {
|
||
loadMkEnabled()
|
||
.then(enabled => {
|
||
this.mkEnabled = !!enabled;
|
||
})
|
||
.catch(() => {
|
||
this.mkEnabled = false;
|
||
});
|
||
},
|
||
methods: {
|
||
hasPermission(code) {
|
||
return this.permission?.[code] !== false;
|
||
},
|
||
unwrapData(response) {
|
||
const body = response?.data || response || {};
|
||
return body?.data || body;
|
||
},
|
||
onLoad(page) {
|
||
this.loading = true;
|
||
api
|
||
.getList(page.currentPage, page.pageSize, { ...this.query })
|
||
.then(res => {
|
||
const data = this.unwrapData(res);
|
||
this.rows = data.records || [];
|
||
this.page.total = Number(data.total || 0);
|
||
})
|
||
.finally(() => {
|
||
this.loading = false;
|
||
this.selection = [];
|
||
this.$refs.crud?.toggleSelection?.();
|
||
});
|
||
},
|
||
searchChange(params, done) {
|
||
// 所属组织级联返回 id 路径数组,转成部门名称给后端
|
||
this.normalizeOrganizationSearch(params, 'deptName');
|
||
// 日期区间拆成后端需要的起止字段
|
||
const range = params.applyDateRange || [];
|
||
delete params.applyDateRange;
|
||
this.query = {
|
||
...params,
|
||
applyStartDate: range[0] || '',
|
||
applyEndDate: range[1] || '',
|
||
};
|
||
this.page.currentPage = 1;
|
||
this.onLoad(this.page);
|
||
done();
|
||
},
|
||
searchReset() {
|
||
this.query = {};
|
||
this.page.currentPage = 1;
|
||
this.onLoad(this.page);
|
||
},
|
||
currentChange(currentPage) {
|
||
this.page.currentPage = currentPage;
|
||
},
|
||
sizeChange(pageSize) {
|
||
this.page.pageSize = pageSize;
|
||
},
|
||
refreshChange() {
|
||
this.onLoad(this.page);
|
||
},
|
||
selectionChange(list) {
|
||
this.selection = list;
|
||
},
|
||
openCreate() {
|
||
this.$router.push({ path: '/payment/payment-application/form', query: { mode: 'add' } });
|
||
},
|
||
openProjectAdvance() {
|
||
this.$router.push({
|
||
path: '/payment/payment-application/form',
|
||
query: { mode: 'add', paymentType: 'progress_advance' },
|
||
});
|
||
},
|
||
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.$router.push({
|
||
path: '/payment/payment-application/form',
|
||
query: { mode: 'add', paymentType: 'settlement_payment' },
|
||
});
|
||
},
|
||
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.refreshChange();
|
||
},
|
||
async handleSubmit(row) {
|
||
const previousApprovalStatus = row.approvalStatus || '';
|
||
await api.submit({ id: row.id });
|
||
await submitMkApprovalFlow({
|
||
bizType: 'payment-application',
|
||
bizId: row.id,
|
||
subjectName: row.paymentNo || '',
|
||
approvalStatus: previousApprovalStatus,
|
||
mkEnabled: this.mkEnabled,
|
||
});
|
||
this.$message.success('提交成功');
|
||
this.refreshChange();
|
||
},
|
||
async handleApprove(row) {
|
||
const res = await api.approve({ id: row.id });
|
||
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||
if (res.data.data === false) {
|
||
this.$message.warning(res.data.msg);
|
||
} else {
|
||
this.$message.success(res.data.msg || '审批通过');
|
||
}
|
||
this.refreshChange();
|
||
},
|
||
handleMkAudit(row) {
|
||
if (!row?.id) {
|
||
this.$message.warning('付款申请ID为空,无法打开审核页');
|
||
return;
|
||
}
|
||
getMKApprovalUrl({ bizId: String(row.id) })
|
||
.then(res => {
|
||
const url = res?.data?.data;
|
||
if (!url) {
|
||
this.$message.error('获取审核跳转地址失败');
|
||
return;
|
||
}
|
||
window.open(url, '_blank');
|
||
})
|
||
.catch(() => {
|
||
this.$message.error('获取审核跳转地址失败');
|
||
});
|
||
},
|
||
async handleReturn(row) {
|
||
const { value } = await this.$prompt('请输入驳回原因', '审批驳回');
|
||
await api.returnBill({ id: row.id, reason: value });
|
||
this.$message.success('已驳回');
|
||
this.refreshChange();
|
||
},
|
||
async handleVoid(row) {
|
||
const { value } = await this.$prompt('请输入作废原因', '作废付款申请');
|
||
await api.voidBill({ id: row.id, reason: value });
|
||
this.$message.success('作废成功');
|
||
this.refreshChange();
|
||
},
|
||
async handleSyncRow(row) {
|
||
const data = this.unwrapData(await api.syncKingdee(row.id));
|
||
this.$message.success(`已生成金蝶付款单:${data}`);
|
||
await this.refreshChange();
|
||
this.warnAttachmentNotUploaded([row.id]);
|
||
},
|
||
async handleSync() {
|
||
if (this.syncLoading) return;
|
||
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
|
||
if (!rows.length) {
|
||
this.$message.warning('请选择至少一条审批通过的付款申请');
|
||
return;
|
||
}
|
||
this.syncLoading = true;
|
||
try {
|
||
const data = this.unwrapData(await api.syncKingdeeBatch(rows.map(row => row.id))) || [];
|
||
const failedList = data.filter(item => String(item).includes('同步失败'));
|
||
if (failedList.length) {
|
||
this.$message({
|
||
type: 'warning',
|
||
message: `同步完成:成功${data.length - failedList.length}条,失败${failedList.length}条。${failedList.join(';')}`,
|
||
duration: 8000,
|
||
});
|
||
} else {
|
||
this.$message.success(`已同步${data.length}条付款申请`);
|
||
}
|
||
await this.refreshChange();
|
||
this.warnAttachmentNotUploaded(rows.map(row => row.id));
|
||
} finally {
|
||
this.syncLoading = false;
|
||
}
|
||
},
|
||
// 附件联带同步提示:同步成功但附件未全部上传时追加 warning,重推即自动补传
|
||
warnAttachmentNotUploaded(ids) {
|
||
const idSet = new Set(ids.map(String));
|
||
const pendingCount = (this.rows || []).filter(row => idSet.has(String(row.id))
|
||
&& ['pending', 'partial'].includes(row.attachmentSyncStatus)).length;
|
||
if (pendingCount) {
|
||
this.$message({
|
||
type: 'warning',
|
||
message: `其中${pendingCount}单附件未全部上传,可重推补传`,
|
||
duration: 8000,
|
||
});
|
||
}
|
||
},
|
||
async handleSyncResult() {
|
||
if (this.resultSyncing) return;
|
||
this.resultSyncing = true;
|
||
try {
|
||
const data = this.unwrapData(await api.syncKingdeeResult());
|
||
this.$message.success(data || '金蝶付款结果回写完成');
|
||
this.loadTable();
|
||
} finally {
|
||
this.resultSyncing = false;
|
||
}
|
||
},
|
||
|
||
handleExport() {
|
||
const columns = this.option.column.filter(column => !column.hide && column.prop);
|
||
const headers = columns.map(item => item.label);
|
||
const rows = this.rows.map(row =>
|
||
columns.map(item =>
|
||
row[item.prop] === null || row[item.prop] === undefined ? '' : 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] ||
|
||
''
|
||
);
|
||
},
|
||
lockTag(row) {
|
||
if (row.kingdeeLockStatus === 'locked') {
|
||
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
|
||
}
|
||
if (row.kingdeeLockStatus === 'failed') {
|
||
if ((row.kingdeeTryCount || 0) >= 5) {
|
||
return {
|
||
show: true,
|
||
type: 'danger',
|
||
label: '锁定失败·已达上限',
|
||
tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁',
|
||
};
|
||
}
|
||
return {
|
||
show: true,
|
||
type: 'warning',
|
||
label: '锁定失败·补偿中',
|
||
tip: row.kingdeeLockFailReason || '系统将自动补偿重锁',
|
||
};
|
||
}
|
||
// 金蝶解锁终态标识:后端按 unlocked 锁记录回填(付款申请不作废,仅此标识可区分金蝶删单与推送失败)
|
||
if (row.kingdeeUnlockTime) {
|
||
const isDelete = row.kingdeeUnlockOperationName === '金蝶删单';
|
||
return {
|
||
show: true,
|
||
type: isDelete ? 'danger' : 'warning',
|
||
label: row.kingdeeUnlockOperationName || '金蝶解锁',
|
||
tip: `${row.kingdeeUnlockOperator || '-'} 于 ${this.$dayjs(row.kingdeeUnlockTime).format('YYYY-MM-DD HH:mm:ss')} 解锁`,
|
||
};
|
||
}
|
||
return { show: false };
|
||
},
|
||
displayValue(value) {
|
||
return value === null || value === undefined || value === '' ? '-' : value;
|
||
},
|
||
formatMoney(value) {
|
||
return Number(value || 0).toFixed(2);
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.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;
|
||
}
|
||
</style>
|