feat(财务): 新增入账管理模块

新增账单导入和账单核对功能
- 添加账单导入页面,支持微信、支付宝、银行账单文件上传
- 实现账单核对功能,包括自动核对和人工核对
- 添加多种类型订单展示组件(VIP、课程、实物商品等)
- 实现订单选择和提交功能
- 添加CardList组件用于展示可选项和已选项
This commit is contained in:
2026-01-06 18:03:12 +08:00
parent 4163a322d7
commit 75111681b4
29 changed files with 3218 additions and 0 deletions

View File

@@ -0,0 +1,331 @@
<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>