- 添加批量对账模式,支持按金额批量选择账单 - 优化rowKey处理逻辑 - 调整对账表单验证规则,增加金额校验 - 修改时间单位从月到天,提高精确度 - 优化选中账单展示,增加标记和计数功能 - 修复SelectDropdownRender组件选项更新问题
450 lines
13 KiB
Vue
450 lines
13 KiB
Vue
<script lang="ts" setup>
|
||
import type { CreateOrderType, PaymentRowType } from '../types';
|
||
|
||
import { ref, watch } from 'vue';
|
||
|
||
import { VbenIcon } from '@vben-core/shadcn-ui';
|
||
|
||
import { Button, Card, Input, 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 TrainingClass from '../components/TrainingClass.vue';
|
||
import VipProduct from '../components/VipProduct.vue';
|
||
import WumenCourseProduct from '../components/WumenCourseProduct.vue';
|
||
import WumenCourseRecord from '../components/WumenCourseRecord.vue';
|
||
import WumenPhysicalProduct from '../components/WumenPhysicalProduct.vue';
|
||
import WumenRecharge from '../components/WumenRecharge.vue';
|
||
import YiluCourseProduct from '../components/YiluCourseProduct.vue';
|
||
import YiluCourseRecord from '../components/YiluCourseRecord.vue';
|
||
import YiluPhysicalProduct from '../components/YiluPhysicalProduct.vue';
|
||
import YiluRecharge from '../components/YiluRecharge.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: WumenRecharge,
|
||
D: WumenPhysicalProduct,
|
||
E: YiluCourseRecord,
|
||
F: YiluCourseProduct,
|
||
G: YiluRecharge,
|
||
H: YiluPhysicalProduct,
|
||
I: VipProduct,
|
||
J: TrainingClass,
|
||
};
|
||
|
||
const sysStore = useSysStore();
|
||
|
||
// 批量模式
|
||
const isBatchMode = ref(false);
|
||
|
||
const visible = ref(props.show);
|
||
watch(visible, (val) => {
|
||
emit('update:show', val);
|
||
});
|
||
|
||
const multipleCurrentIndex = ref<number[]>([0]);
|
||
const multipleCurrentData = ref<PaymentRowType[]>([]);
|
||
const selectedAmount = ref();
|
||
const pendingData = ref<PaymentRowType[]>([]);
|
||
|
||
// 监听props.data变化,更新本地pendingData
|
||
watch(
|
||
() => props.data,
|
||
(newData) => {
|
||
pendingData.value = newData.toSorted(
|
||
(a, b) => Number(b.orderFlag === 1) - Number(a.orderFlag === 1),
|
||
);
|
||
multipleCurrentIndex.value = [0];
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
function changePendingData(index: number) {
|
||
// 单个模式
|
||
multipleCurrentIndex.value = [index];
|
||
selectedData.value = [];
|
||
selectedRef.value?.clearData();
|
||
selectedAmount.value = null;
|
||
isBatchMode.value = false;
|
||
}
|
||
|
||
function changeBatchSelected() {
|
||
selectedData.value = [];
|
||
selectedRef.value?.clearData();
|
||
|
||
if (selectedAmount.value || selectedAmount.value === '0') {
|
||
// 按金额批量选中
|
||
activeTabKey.value = 'B'; // 批量模式不能选订单和记录,默认到课程商品
|
||
isBatchMode.value = true;
|
||
multipleCurrentData.value = pendingData.value.filter(
|
||
(item) => item.fee === Number.parseFloat(selectedAmount.value),
|
||
);
|
||
multipleCurrentIndex.value = multipleCurrentData.value.map((item) =>
|
||
pendingData.value.indexOf(item),
|
||
);
|
||
} else {
|
||
// 取消批量选中
|
||
activeTabKey.value = '0'; // 切换到订单列表
|
||
isBatchMode.value = false;
|
||
changePendingData(0);
|
||
}
|
||
}
|
||
|
||
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) {
|
||
if (isBatchMode.value && ['0', 'A', 'C', 'E', 'G'].includes(key)) {
|
||
message.warning('批量模式下,不能切换到该选项');
|
||
return;
|
||
}
|
||
activeTabKey.value = key;
|
||
}
|
||
|
||
// 确认对账完成
|
||
function onCompleteCheck() {
|
||
notification.success({
|
||
message: '操作成功',
|
||
duration: 1,
|
||
});
|
||
// 删除当前已对账的数据
|
||
const currentIndex = multipleCurrentIndex.value[0] || 0;
|
||
multipleCurrentIndex.value.forEach((index) => {
|
||
pendingData.value.splice(index, 1);
|
||
});
|
||
|
||
// 如果没有待对账数据了,关闭弹框
|
||
if (pendingData.value.length === 0) {
|
||
visible.value = false;
|
||
return;
|
||
}
|
||
|
||
// 清空待处理账单的已选状态
|
||
multipleCurrentIndex.value = [];
|
||
multipleCurrentData.value = [];
|
||
// 如果当前索引超出了数据范围,重置到第一条,否则下一条(删除已处理索引后,currentIndex即是下一条)
|
||
multipleCurrentIndex.value[0] = Math.min(currentIndex, pendingData.value.length - 1);
|
||
}
|
||
// 其他方式--确认对账 --------------
|
||
// 已选列表
|
||
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 completeSubmitCheck(item: CreateOrderType) {
|
||
if (!item.realMoney) {
|
||
message.error(`《${item.productName}》请填写实际金额`);
|
||
return true;
|
||
}
|
||
if (!item.orderMoney) {
|
||
message.error(`《${item.productName}》请填写订单金额`);
|
||
return true;
|
||
}
|
||
// 单个模式必须填手机号
|
||
if (!isBatchMode.value && !item.tel) {
|
||
message.error(`《${item.productName}》请填写手机号`);
|
||
return true;
|
||
}
|
||
// 单个模式课程、vip必须填开始时间和结束时间
|
||
if (!isBatchMode.value && (item.orderType === '1' || item.orderType === '2') && !item.startTime) {
|
||
message.error(`《${item.productName}》请填写开始时间`);
|
||
return true;
|
||
}
|
||
if (!isBatchMode.value && (item.orderType === '1' || item.orderType === '2') && !item.endTime) {
|
||
message.error(`《${item.productName}》请填写到期时间`);
|
||
return true;
|
||
}
|
||
// 批量模式课程、vip必须填到期时间
|
||
if (isBatchMode.value && (item.orderType === '1' || item.orderType === '2') && !item.endTime) {
|
||
message.error(`《${item.productName}》请填写到期时间`);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
// 确认对账
|
||
async function onCompleteCheckCreated() {
|
||
if (multipleCurrentIndex.value.length === 0) {
|
||
message.error('请选择账单');
|
||
return;
|
||
}
|
||
if (selectedData.value.length === 0) {
|
||
message.error('请选择对账数据');
|
||
}
|
||
|
||
const selected = selectedRef.value.getData();
|
||
|
||
// 检查已选数据中是否有未填写的表单项
|
||
const emptyItem = selected.find((item: CreateOrderType) => {
|
||
return completeSubmitCheck(item);
|
||
});
|
||
if (emptyItem) {
|
||
errorData.value = [emptyItem];
|
||
return;
|
||
} else {
|
||
errorData.value = [];
|
||
}
|
||
|
||
// 验证订单金额不能小于实际金额
|
||
const invalidItem = selected.find((item: CreateOrderType) => {
|
||
return Number.parseFloat(item.orderMoney) < Number.parseFloat(item.realMoney);
|
||
});
|
||
if (invalidItem) {
|
||
errorData.value = [invalidItem];
|
||
message.error(`《${invalidItem.productName}》订单金额不能小于实际金额`);
|
||
return;
|
||
} else {
|
||
errorData.value = [];
|
||
}
|
||
|
||
// 判断已选的数据填写的实际金额总和是否与对账单金额一致
|
||
const totalRealMoney = selected.reduce(
|
||
(acc: number, item: CreateOrderType) => acc + Number.parseFloat(item.realMoney),
|
||
0,
|
||
);
|
||
if (totalRealMoney !== pendingData.value?.[multipleCurrentIndex.value[0] ?? 0]?.fee) {
|
||
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 (!isBatchMode.value && (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), 'day')
|
||
.format('YYYY-MM-DD'); // 处理结束时间,根据传入的vb时间和课程时长计算
|
||
}
|
||
|
||
// 如果是多选模式,处理相应数据
|
||
if (isBatchMode.value) {
|
||
delete result.paymentId;
|
||
result.startTime = '';
|
||
}
|
||
|
||
return result;
|
||
});
|
||
|
||
const paymentIds = multipleCurrentData.value.map((item) => item.id);
|
||
// 调用接口
|
||
await (isBatchMode.value
|
||
? reconciliateBillsApi.manualCheckCreatedBatch({
|
||
paymentIds: paymentIds.join(','),
|
||
orders: data,
|
||
})
|
||
: reconciliateBillsApi.manualCheckCreated(data));
|
||
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 items-center justify-between p-1 pb-2">
|
||
<Input
|
||
v-model:value="selectedAmount"
|
||
addon-before="批量选中金额"
|
||
allow-clear
|
||
class="w-60"
|
||
@change="changeBatchSelected"
|
||
/>
|
||
<div class="ml-2">已选中:{{ multipleCurrentIndex.length }}</div>
|
||
</div>
|
||
<div class="flex flex-col gap-2 overflow-y-auto px-1" style="height: calc(100vh - 170px)">
|
||
<div
|
||
v-for="(item, index) in pendingData"
|
||
:key="item.id"
|
||
:class="{ active: multipleCurrentIndex.includes(index) }"
|
||
class="pay-order-item relative"
|
||
@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>
|
||
|
||
<VbenIcon
|
||
v-if="item.orderFlag === 1"
|
||
icon="ant-design:star-fill"
|
||
class="absolute right-1 top-1 h-5 w-5 text-red-500"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
<div class="flex w-3/4 flex-1 flex-col gap-2">
|
||
<Card
|
||
:tab-list="tabList"
|
||
disabled
|
||
:active-tab-key="activeTabKey"
|
||
@tab-change="changeRecordTab"
|
||
class="order-card flex-1"
|
||
size="small"
|
||
>
|
||
<Orders
|
||
v-if="activeTabKey === '0'"
|
||
:payment-id="pendingData[multipleCurrentIndex[0] ?? 0]?.id"
|
||
@complete-check="onCompleteCheck"
|
||
/>
|
||
<component
|
||
:is="needCreateRecord[activeTabKey]"
|
||
v-if="activeTabKey !== '0'"
|
||
ref="needCreateRecordRef"
|
||
:tab-key="activeTabKey"
|
||
:payment="pendingData[multipleCurrentIndex[0] ?? 0]"
|
||
:selected-data="selectedData"
|
||
@complete-check="onSelectRecord"
|
||
@deleted-checked="onDeleteChecked"
|
||
/>
|
||
</Card>
|
||
<Card
|
||
v-if="activeTabKey !== '0'"
|
||
:title="`已选列表(${selectedData.length})`"
|
||
class="h-[270px] w-full"
|
||
size="small"
|
||
>
|
||
<template #extra>
|
||
<Button type="primary" size="small" @click="onCompleteCheckCreated()">确认对账</Button>
|
||
</template>
|
||
<Selected
|
||
ref="selectedRef"
|
||
:error-item="errorData"
|
||
:is-batch-mode="isBatchMode"
|
||
@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;
|
||
padding-bottom: 10px !important;
|
||
}
|
||
.order-card {
|
||
width: 100%;
|
||
:deep(.ant-card-body) {
|
||
padding: 1px !important;
|
||
height: calc(100% - 41px); // 减去标签页头部高度
|
||
// background-color: #f1f3f6;
|
||
}
|
||
}
|
||
</style>
|