Files
finance-master/apps/finance/src/views/posting/reconciliate/modules/Manual.vue
chenghuan 75111681b4 feat(财务): 新增入账管理模块
新增账单导入和账单核对功能
- 添加账单导入页面,支持微信、支付宝、银行账单文件上传
- 实现账单核对功能,包括自动核对和人工核对
- 添加多种类型订单展示组件(VIP、课程、实物商品等)
- 实现订单选择和提交功能
- 添加CardList组件用于展示可选项和已选项
2026-01-06 18:03:12 +08:00

332 lines
9.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
import type { CreateOrderType, PaymentRowType } from '../types';
import { ref, watch } from 'vue';
import { Button, Card, message, Modal, notification } from 'ant-design-vue';
import dayjs from 'dayjs';
import { reconciliateBillsApi } from '#/api/posting/reconciliate';
import { useSysStore } from '#/store';
import Orders from '../components/Orders.vue';
import Selected from '../components/Selected.vue';
import TrainingClassProduct from '../components/TrainingClassProduct.vue';
import VipProduct from '../components/VipProduct.vue';
import WumenCourseProduct from '../components/WumenCourseProduct.vue';
import WumenCourseRecharge from '../components/WumenCourseRecharge.vue';
import WumenCourseRecord from '../components/WumenCourseRecord.vue';
import WumenPhysicalProduct from '../components/WumenPhysicalProduct.vue';
import YiluCourseProduct from '../components/YiluCourseProduct.vue';
import YiluCourseRecharge from '../components/YiluCourseRecharge.vue';
import YiluCourseRecord from '../components/YiluCourseRecord.vue';
import YiluPhysicalProduct from '../components/YiluPhysicalProduct.vue';
const props = withDefaults(
defineProps<{
data?: PaymentRowType[];
show?: boolean;
}>(),
{
show: false,
data: () => [],
},
);
const emit = defineEmits(['update:show']);
const needCreateRecord: Record<string, any> = {
A: WumenCourseRecord,
B: WumenCourseProduct,
C: WumenCourseRecharge,
D: WumenPhysicalProduct,
E: YiluCourseRecord,
F: YiluCourseProduct,
G: YiluCourseRecharge,
H: YiluPhysicalProduct,
I: VipProduct,
J: TrainingClassProduct,
};
const sysStore = useSysStore();
const visible = ref(props.show);
watch(visible, (val) => {
emit('update:show', val);
});
const currentIndex = ref(0);
const pendingData = ref<PaymentRowType[]>([]);
// 监听props.data变化更新本地pendingData
watch(
() => props.data,
(newData) => {
pendingData.value = [...newData];
currentIndex.value = 0;
},
{ immediate: true },
);
function changePendingData(index: number) {
currentIndex.value = index;
selectedData.value = [];
selectedRef.value?.clearData();
}
const tabList = [
{
key: '0',
tab: '订单列表',
},
{
key: 'A',
tab: '吴门医述开课记录',
},
{
key: 'B',
tab: '吴门医述课程商品',
},
{
key: 'C',
tab: '吴门医述充值记录',
},
{
key: 'D',
tab: '吴门医述实物商品',
},
{
key: 'E',
tab: '一路健康开课记录',
},
{
key: 'F',
tab: '一路健康课程商品',
},
{
key: 'G',
tab: '一路健康充值记录',
},
{
key: 'H',
tab: '一路健康实物商品',
},
{
key: 'I',
tab: '开通VIP',
},
{
key: 'J',
tab: '报名培训班',
},
];
const activeTabKey = ref('0');
function changeRecordTab(key: string) {
activeTabKey.value = key;
}
// 确认对账完成
function onCompleteCheck() {
notification.success({
message: '操作成功',
});
// 删除当前已对账的数据
pendingData.value.splice(currentIndex.value, 1);
// 如果没有待对账数据了,关闭弹框
if (pendingData.value.length === 0) {
visible.value = false;
return;
}
// 如果当前索引超出了数据范围,重置到第一条
if (currentIndex.value >= pendingData.value.length) {
currentIndex.value = 0;
}
}
// 其他方式--确认对账 --------------
// 已选列表
const selectedRef = ref();
const errorData = ref<CreateOrderType[]>([]);
const selectedData = ref<CreateOrderType[]>([]);
const needCreateRecordRef = ref();
// 选中
function onSelectRecord(rows: CreateOrderType[]) {
const addData = rows.filter(
(item) => !selectedData.value.some((selected) => selected.id === item.id),
);
selectedData.value.push(...addData);
selectedRef.value?.handleAdd(addData);
}
// 取消选中
function onDeleteChecked(ids: number[]) {
ids.forEach((id) => {
const rowData = selectedData.value.find((item) => item.id === id);
const index = selectedData.value.findIndex((item) => item.id === id);
if (index !== -1) {
selectedData.value.splice(index, 1);
selectedRef.value?.deleteData(id);
}
if (rowData && needCreateRecordRef.value) {
needCreateRecordRef.value.cancelCheck(id);
}
});
}
// 确认对账
function onCompleteCheckCreated() {
if (selectedData.value.length === 0) {
message.error('请选择数据');
return;
}
const selected = selectedRef.value.getData();
// 检查已选数据中是否有未填写的表单项
const emptyItem = selected.find((item: CreateOrderType) => {
if (
!item.realMoney ||
!item.orderMoney ||
!item.tel ||
((item.orderType === '1' || item.orderType === '2') && (!item.startTime || !item.endTime))
) {
return true;
}
return false;
});
if (emptyItem) {
errorData.value = [emptyItem];
message.error(`${emptyItem.productName}》有未填写项`);
return;
} else {
errorData.value = [];
}
// 判断已选的数据填写的实际金额总和是否与对账单金额一致
const totalRealMoney = selected.reduce(
(acc: number, item: CreateOrderType) => acc + Number.parseFloat(item.realMoney),
0,
);
if (totalRealMoney !== Number.parseFloat(pendingData.value?.[currentIndex.value]?.fee || '0')) {
message.error('已选数据填写的实际金额总和与对账单金额不一致');
return;
}
const data = selected.map((item: CreateOrderType) => {
const result: any = {
...item,
districtMoney: (
Number.parseFloat(item.orderMoney) - Number.parseFloat(item.realMoney)
).toString(),
};
// 只有VIP和课程订单才处理开始时间和结束时间
if (item.orderType === '1' || item.orderType === '2') {
result.startTime = dayjs(item.startTime).format('YYYY-MM-DD'); // 格式化开始时间
result.endTime = dayjs(item.startTime)
.add(Number.parseFloat(item.endTime), 'month')
.format('YYYY-MM-DD'); // 处理结束时间根据传入的vb时间和课程时长计算
}
return result;
});
// 调用接口
reconciliateBillsApi.manualCheckCreated(data).then(() => {
onCompleteCheck();
// 清空已选列表
selectedData.value = [];
selectedRef.value?.clearData();
});
}
</script>
<template>
<Modal
v-model:open="visible"
width="100%"
title="人工对账"
:footer="null"
centered
wrap-class-name="full-modal"
>
<div class="flex gap-2">
<Card title="待对账列表" class="w-1/4" size="small">
<div class="flex flex-col gap-2 overflow-y-auto p-1" style="height: calc(100vh - 120px)">
<div
v-for="(item, index) in pendingData"
:key="item.id"
:class="{ active: currentIndex === index }"
class="pay-order-item"
@click="changePendingData(index)"
>
<div>关联sn号{{ item.relationSn }}</div>
<div>业务流水号{{ item.transactionSn }}</div>
<div>财务流水号{{ item.financeSn }}</div>
<div>金额{{ item.fee }}</div>
<div>支付方式{{ sysStore.getDictMap('payment', item.type) }}</div>
<div>时间{{ dayjs(item.ctime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</div>
</div>
</Card>
<div class="flex flex-1 flex-col gap-2">
<Card
:tab-list="tabList"
:active-tab-key="activeTabKey"
@tab-change="changeRecordTab"
class="order-card flex-1"
size="small"
>
<Orders
v-if="activeTabKey === '0'"
:payment-id="pendingData[currentIndex]?.id"
@complete-check="onCompleteCheck"
/>
<component
:is="needCreateRecord[activeTabKey]"
v-if="activeTabKey !== '0'"
ref="needCreateRecordRef"
:tab-key="activeTabKey"
:payment="pendingData[currentIndex]"
:selected-data="selectedData"
@complete-check="onSelectRecord"
@deleted-checked="onDeleteChecked"
/>
</Card>
<Card
v-if="activeTabKey !== '0'"
:title="`已选列表(${selectedData.length})`"
class="h-[405px] w-full"
size="small"
>
<template #extra>
<Button type="primary" size="small" @click="onCompleteCheckCreated()">确认对账</Button>
</template>
<Selected ref="selectedRef" :error-item="errorData" @deleted-checked="onDeleteChecked" />
</Card>
</div>
</div>
</Modal>
</template>
<style lang="scss" scoped>
.pay-order-item {
padding: 10px;
border: 1px solid #d9d9d9;
border-radius: 10px;
&.active {
padding: 9px;
border: 2px solid #1890ff;
background-color: rgba(24, 144, 255, 0.05);
}
}
:deep(.ant-card-body) {
padding: 5px !important;
}
.order-card {
:deep(.ant-card-body) {
padding: 1px !important;
height: calc(100% - 46px); // 减去标签页头部高度
// background-color: #f1f3f6;
}
}
</style>