diff --git a/apps/finance/src/api/posting/reconciliate.ts b/apps/finance/src/api/posting/reconciliate.ts index b5906ed..96f16e2 100644 --- a/apps/finance/src/api/posting/reconciliate.ts +++ b/apps/finance/src/api/posting/reconciliate.ts @@ -16,12 +16,19 @@ export const reconciliateBillsApi = { }, /** - * 人工对账--添加订单后对账 + * 人工对账--添加订单后对账 -- 单个 */ manualCheckCreated: (data: any) => { return requestClient.post('/common/payment/checkoffByAddOrder', { list: data }); }, + /** + * 人工对账--添加订单后对账 -- 批量 + */ + manualCheckCreatedBatch: (data: any) => { + return requestClient.post('/common/payment/checkoffByBatch', data); + }, + /** * 自动对账 */ diff --git a/apps/finance/src/components/SelectDropdownRender/index.vue b/apps/finance/src/components/SelectDropdownRender/index.vue index c0586a9..2b54a32 100644 --- a/apps/finance/src/components/SelectDropdownRender/index.vue +++ b/apps/finance/src/components/SelectDropdownRender/index.vue @@ -34,16 +34,18 @@ const VNodes = defineComponent({ }); const items = ref<{ value: string }[]>(props.options || []); -const value = ref(props.value); +// const value = computed(() => props.value || items.value[0]?.value); +const value = ref(props.value || items.value[0]?.value); const inputRef = ref(); const inputValue = ref(''); // 监听 props.value 变化,更新本地 value watch( - () => props.value, + () => props.options, (newValue) => { - value.value = newValue; + value.value = props.value && props.value !== '' ? props.value : newValue.value[0]?.value; }, + { immediate: true, deep: true }, ); // 监听本地 value 变化,触发 update:value 事件 diff --git a/apps/finance/src/components/card-list/card-list.vue b/apps/finance/src/components/card-list/card-list.vue index 862946a..3754913 100644 --- a/apps/finance/src/components/card-list/card-list.vue +++ b/apps/finance/src/components/card-list/card-list.vue @@ -191,7 +191,7 @@ const renderEditComponent = (column: any, row: any) => { const options = ref([]); // 创建一个唯一的缓存键,基于 row 的关键属性 - const cacheKey = `${row.paymentId}_${row.come}_${row.orderType}_${row.courseId}`; + const cacheKey = `${row.paymentId}_${row[gridOptions.value?.cardConfig?.keyField || 'id']}`; // 检查缓存中是否已有数据 if (selectOptionsCache[cacheKey]) { diff --git a/apps/finance/src/components/card-list/types.ts b/apps/finance/src/components/card-list/types.ts index 31137cf..221f53b 100644 --- a/apps/finance/src/components/card-list/types.ts +++ b/apps/finance/src/components/card-list/types.ts @@ -41,6 +41,11 @@ export interface CardListPagination { export interface CardListOptions { /** 列配置 */ columns?: CardListColumn[]; + /** 卡片配置 */ + cardConfig?: { + /** 卡片主键字段名 */ + keyField?: string; + }; /** 数据源 */ data?: T[]; /** 是否显示卡片标题 */ diff --git a/apps/finance/src/views/posting/reconciliate/components/Selected.vue b/apps/finance/src/views/posting/reconciliate/components/Selected.vue index af510e5..3f1c64b 100644 --- a/apps/finance/src/views/posting/reconciliate/components/Selected.vue +++ b/apps/finance/src/views/posting/reconciliate/components/Selected.vue @@ -15,6 +15,7 @@ interface RecommendedUser { const props = withDefaults( defineProps<{ errorItem: CreateOrderType[]; + isBatchMode: boolean; }>(), {}, ); @@ -73,7 +74,7 @@ const gridOptions = computed(() => ({ paymentId: row.paymentId, come: row.come, orderType: row.orderType, - courseId: row.courseId, + courseId: row.courseId ?? '', vipType: row.vipType ?? '', }) .then( @@ -88,6 +89,7 @@ const gridOptions = computed(() => ({ field: 'tel', title: '用户手机号', colSpan: 2, + show: () => !props.isBatchMode, }, { editRender: { name: 'Input' }, field: 'orderMoney', title: '订单金额' }, { editRender: { name: 'Input' }, field: 'realMoney', title: '实际金额' }, @@ -99,7 +101,8 @@ const gridOptions = computed(() => ({ }, }, // 仅VIP 和课程订单 显示 - show: (row: CreateOrderType) => row.orderType === '1' || row.orderType === '2', + show: (row: CreateOrderType) => + !props.isBatchMode && (row.orderType === '1' || row.orderType === '2'), field: 'startTime', title: '开始时间', }, @@ -108,13 +111,13 @@ const gridOptions = computed(() => ({ name: 'Select', props: { options: [ - { label: '1个月', value: '1' }, - { label: '3个月', value: '3' }, - { label: '半年', value: '6' }, - { label: '一年', value: '12' }, - { label: '两年', value: '24' }, - { label: '三年', value: '36' }, - { label: '四年', value: '48' }, + { label: '1个月', value: '30' }, + { label: '3个月', value: '90' }, + { label: '半年', value: '180' }, + { label: '一年', value: '365' }, + { label: '两年', value: '730' }, + { label: '三年', value: '1095' }, + { label: '四年', value: '1460' }, ], }, }, @@ -124,6 +127,9 @@ const gridOptions = computed(() => ({ title: '到期时间', }, ], + cardConfig: { + keyField: 'id', + }, showTitle: true, titleField: 'productName', gridColumns: 3, diff --git a/apps/finance/src/views/posting/reconciliate/components/TrainingClassProduct.vue b/apps/finance/src/views/posting/reconciliate/components/TrainingClass.vue similarity index 93% rename from apps/finance/src/views/posting/reconciliate/components/TrainingClassProduct.vue rename to apps/finance/src/views/posting/reconciliate/components/TrainingClass.vue index 68a33cf..2df47ee 100644 --- a/apps/finance/src/views/posting/reconciliate/components/TrainingClassProduct.vue +++ b/apps/finance/src/views/posting/reconciliate/components/TrainingClass.vue @@ -20,7 +20,8 @@ const props = withDefaults( const emit = defineEmits(['completeCheck', 'deletedChecked']); interface RowType { - productId: number; + id: string; + productId: string; year: string; title: string; } @@ -34,10 +35,10 @@ function transformData(rows: RowType[]) { orderType: '4', paymentId: props.payment?.id || '', productName: row.title, - productId: row.productId, + productId: row.productId.slice(1), catalogueId: '', orderMoney: '', - realMoney: '', + realMoney: String(props.payment?.fee) || '', districtMoney: '', startTime: '', endTime: '', diff --git a/apps/finance/src/views/posting/reconciliate/components/VipProduct.vue b/apps/finance/src/views/posting/reconciliate/components/VipProduct.vue index 1fe0942..c3caaf6 100644 --- a/apps/finance/src/views/posting/reconciliate/components/VipProduct.vue +++ b/apps/finance/src/views/posting/reconciliate/components/VipProduct.vue @@ -35,11 +35,11 @@ function transformData(rows: RowType[]) { productId: '', catalogueId: '', orderMoney: '', - realMoney: '', + realMoney: String(props.payment?.fee) || '', districtMoney: '', startTime: props.payment?.ctime || '', endTime: '', - vipType: row.type, + vipType: row.type.slice(1), })); } diff --git a/apps/finance/src/views/posting/reconciliate/components/WumenCourseProduct.vue b/apps/finance/src/views/posting/reconciliate/components/WumenCourseProduct.vue index 898dbcf..1cf832c 100644 --- a/apps/finance/src/views/posting/reconciliate/components/WumenCourseProduct.vue +++ b/apps/finance/src/views/posting/reconciliate/components/WumenCourseProduct.vue @@ -20,8 +20,8 @@ const props = withDefaults( const emit = defineEmits(['completeCheck', 'deletedChecked']); interface RowType { - courseId: number; - productId: number; + courseId: string; + productId: string; price: number; productName: string; } @@ -35,10 +35,10 @@ function transformData(rows: RowType[]) { orderType: '2', paymentId: props.payment?.id || '', productName: row.productName, - productId: row.productId, + productId: row.productId.slice(1), catalogueId: '', orderMoney: String(row.price), - realMoney: '', + realMoney: String(props.payment?.fee) || '', districtMoney: '', startTime: props.payment?.ctime || '', endTime: '', diff --git a/apps/finance/src/views/posting/reconciliate/components/WumenCourseRecord.vue b/apps/finance/src/views/posting/reconciliate/components/WumenCourseRecord.vue index 8175f0b..431f686 100644 --- a/apps/finance/src/views/posting/reconciliate/components/WumenCourseRecord.vue +++ b/apps/finance/src/views/posting/reconciliate/components/WumenCourseRecord.vue @@ -40,7 +40,7 @@ function transformData(rows: RowType[]) { productId: '', catalogueId: row.catalogueId, orderMoney: '', - realMoney: '', + realMoney: String(props.payment?.fee) || '', districtMoney: '', startTime: props.payment?.ctime || '', endTime: '', diff --git a/apps/finance/src/views/posting/reconciliate/components/WumenPhysicalProduct.vue b/apps/finance/src/views/posting/reconciliate/components/WumenPhysicalProduct.vue index 42ff615..506551f 100644 --- a/apps/finance/src/views/posting/reconciliate/components/WumenPhysicalProduct.vue +++ b/apps/finance/src/views/posting/reconciliate/components/WumenPhysicalProduct.vue @@ -20,7 +20,7 @@ const props = withDefaults( const emit = defineEmits(['completeCheck', 'deletedChecked']); interface RowType { - id: number; + id: string; price: number; productName: string; } @@ -34,10 +34,10 @@ function transformData(rows: RowType[]) { orderType: '3', paymentId: props.payment?.id || '', productName: row.productName, - productId: row.id, + productId: row.id.slice(1), catalogueId: '', orderMoney: String(row.price), - realMoney: '', + realMoney: String(props.payment?.fee) || '', districtMoney: '', startTime: '', endTime: '', diff --git a/apps/finance/src/views/posting/reconciliate/components/WumenRecharge.vue b/apps/finance/src/views/posting/reconciliate/components/WumenRecharge.vue index 5e30fe0..4cd5354 100644 --- a/apps/finance/src/views/posting/reconciliate/components/WumenRecharge.vue +++ b/apps/finance/src/views/posting/reconciliate/components/WumenRecharge.vue @@ -37,7 +37,7 @@ function transformData(rows: RowType[]) { productId: '', catalogueId: '', orderMoney: String(row.point), - realMoney: '', + realMoney: String(props.payment?.fee) || '', districtMoney: '', startTime: '', endTime: '', diff --git a/apps/finance/src/views/posting/reconciliate/components/YiluCourseProduct.vue b/apps/finance/src/views/posting/reconciliate/components/YiluCourseProduct.vue index acc45af..5e899e8 100644 --- a/apps/finance/src/views/posting/reconciliate/components/YiluCourseProduct.vue +++ b/apps/finance/src/views/posting/reconciliate/components/YiluCourseProduct.vue @@ -28,7 +28,7 @@ interface RowType { function transformData(rows: RowType[]) { return rows.map((row) => ({ id: row.courseId, - courseId: row.courseId || '', + courseId: row.courseId.slice(1), tel: '', come: '0', orderType: '2', @@ -37,7 +37,7 @@ function transformData(rows: RowType[]) { productId: '', catalogueId: '', orderMoney: '', - realMoney: '', + realMoney: String(props.payment?.fee) || '', districtMoney: '', startTime: props.payment?.ctime || '', endTime: '', diff --git a/apps/finance/src/views/posting/reconciliate/components/YiluCourseRecord.vue b/apps/finance/src/views/posting/reconciliate/components/YiluCourseRecord.vue index 0ffe8e5..dcffde4 100644 --- a/apps/finance/src/views/posting/reconciliate/components/YiluCourseRecord.vue +++ b/apps/finance/src/views/posting/reconciliate/components/YiluCourseRecord.vue @@ -19,6 +19,7 @@ const props = withDefaults( const emit = defineEmits(['completeCheck', 'deletedChecked']); interface RowType { + oid: string; courseFee: string; courseId: string; studyDays: number; @@ -29,9 +30,9 @@ interface RowType { function transformData(rows: RowType[]) { return rows.map((row) => ({ - id: row.courseId, + id: row.oid, paymentId: props.payment?.id || 0, - courseId: row.courseId, + courseId: row.courseId.slice(1), tel: row.cellPhone, come: '0', orderType: '2', @@ -39,7 +40,7 @@ function transformData(rows: RowType[]) { productId: '', catalogueId: '', orderMoney: '', - realMoney: '', + realMoney: String(props.payment?.fee) || '', districtMoney: '', startTime: props.payment?.ctime || '', endTime: '', @@ -49,7 +50,7 @@ function transformData(rows: RowType[]) { const { Grid, cancelCheck, setChecked } = useCardListGrid({ payment: computed(() => props.payment), selectedData: computed(() => props.selectedData), - rowKey: 'courseId', + rowKey: 'oid', rowKeyPrefix: props.tabKey, columns: [ { type: 'checkbox', width: 60 }, diff --git a/apps/finance/src/views/posting/reconciliate/components/YiluPhysicalProduct.vue b/apps/finance/src/views/posting/reconciliate/components/YiluPhysicalProduct.vue index 6a42bcf..a4ab19d 100644 --- a/apps/finance/src/views/posting/reconciliate/components/YiluPhysicalProduct.vue +++ b/apps/finance/src/views/posting/reconciliate/components/YiluPhysicalProduct.vue @@ -34,10 +34,10 @@ function transformData(rows: RowType[]) { orderType: '3', paymentId: props.payment?.id || '', productName: row.productName, - productId: row.id, + productId: row.id.slice(1), catalogueId: '', orderMoney: String(row.price), - realMoney: '', + realMoney: String(props.payment?.fee) || '', districtMoney: '', startTime: '', endTime: '', diff --git a/apps/finance/src/views/posting/reconciliate/components/YiluRecharge.vue b/apps/finance/src/views/posting/reconciliate/components/YiluRecharge.vue index 4c0f29d..3cb1381 100644 --- a/apps/finance/src/views/posting/reconciliate/components/YiluRecharge.vue +++ b/apps/finance/src/views/posting/reconciliate/components/YiluRecharge.vue @@ -37,7 +37,7 @@ function transformData(rows: RowType[]) { productId: '', catalogueId: '', orderMoney: String(row.point), - realMoney: '', + realMoney: String(props.payment?.fee) || '', districtMoney: '', startTime: '', endTime: '', diff --git a/apps/finance/src/views/posting/reconciliate/modules/Manual.vue b/apps/finance/src/views/posting/reconciliate/modules/Manual.vue index 6f2a7de..a915df7 100644 --- a/apps/finance/src/views/posting/reconciliate/modules/Manual.vue +++ b/apps/finance/src/views/posting/reconciliate/modules/Manual.vue @@ -3,7 +3,9 @@ import type { CreateOrderType, PaymentRowType } from '../types'; import { ref, watch } from 'vue'; -import { Button, Card, message, Modal, notification } from 'ant-design-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'; @@ -11,7 +13,7 @@ import { useSysStore } from '#/store'; import Orders from '../components/Orders.vue'; import Selected from '../components/Selected.vue'; -import TrainingClassProduct from '../components/TrainingClassProduct.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'; @@ -45,33 +47,65 @@ const needCreateRecord: Record = { G: YiluRecharge, H: YiluPhysicalProduct, I: VipProduct, - J: TrainingClassProduct, + J: TrainingClass, }; const sysStore = useSysStore(); +// 批量模式 +const isBatchMode = ref(false); + const visible = ref(props.show); watch(visible, (val) => { emit('update:show', val); }); -const currentIndex = ref(0); +const multipleCurrentIndex = ref([0]); +const multipleCurrentData = ref([]); +const selectedAmount = ref(); const pendingData = ref([]); // 监听props.data变化,更新本地pendingData watch( () => props.data, (newData) => { - pendingData.value = [...newData]; - currentIndex.value = 0; + pendingData.value = newData.toSorted( + (a, b) => Number(b.orderFlag === 1) - Number(a.orderFlag === 1), + ); + multipleCurrentIndex.value = [0]; }, { immediate: true }, ); function changePendingData(index: number) { - currentIndex.value = index; + // 单个模式 + 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 = [ @@ -122,6 +156,10 @@ const tabList = [ ]; const activeTabKey = ref('0'); function changeRecordTab(key: string) { + if (isBatchMode.value && ['0', 'A', 'C', 'E', 'G'].includes(key)) { + message.warning('批量模式下,不能切换到该选项'); + return; + } activeTabKey.value = key; } @@ -129,9 +167,13 @@ function changeRecordTab(key: string) { function onCompleteCheck() { notification.success({ message: '操作成功', + duration: 1, }); // 删除当前已对账的数据 - pendingData.value.splice(currentIndex.value, 1); + const currentIndex = multipleCurrentIndex.value[0] || 0; + multipleCurrentIndex.value.forEach((index) => { + pendingData.value.splice(index, 1); + }); // 如果没有待对账数据了,关闭弹框 if (pendingData.value.length === 0) { @@ -139,10 +181,11 @@ function onCompleteCheck() { return; } - // 如果当前索引超出了数据范围,重置到第一条 - if (currentIndex.value >= pendingData.value.length) { - currentIndex.value = 0; - } + // 清空待处理账单的已选状态 + multipleCurrentIndex.value = []; + multipleCurrentData.value = []; + // 如果当前索引超出了数据范围,重置到第一条,否则下一条(删除已处理索引后,currentIndex即是下一条) + multipleCurrentIndex.value[0] = Math.min(currentIndex, pendingData.value.length - 1); } // 其他方式--确认对账 -------------- // 已选列表 @@ -172,29 +215,68 @@ function onDeleteChecked(ids: number[]) { } }); } + +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; +} // 确认对账 -function onCompleteCheckCreated() { - if (selectedData.value.length === 0) { - message.error('请选择数据'); +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) => { - if ( - !item.realMoney || - !item.orderMoney || - !item.tel || - ((item.orderType === '1' || item.orderType === '2') && (!item.startTime || !item.endTime)) - ) { - return true; - } - return false; + return completeSubmitCheck(item); }); if (emptyItem) { errorData.value = [emptyItem]; - message.error(`《${emptyItem.productName}》有未填写项`); + 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 = []; @@ -205,7 +287,7 @@ function onCompleteCheckCreated() { (acc: number, item: CreateOrderType) => acc + Number.parseFloat(item.realMoney), 0, ); - if (totalRealMoney !== Number.parseFloat(pendingData.value?.[currentIndex.value]?.fee || '0')) { + if (totalRealMoney !== pendingData.value?.[multipleCurrentIndex.value[0] ?? 0]?.fee) { message.error('已选数据填写的实际金额总和与对账单金额不一致'); return; } @@ -218,23 +300,35 @@ function onCompleteCheckCreated() { ).toString(), }; - // 只有VIP和课程订单才处理开始时间和结束时间 - if (item.orderType === '1' || item.orderType === '2') { + // 单选模式且只有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), 'month') + .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); // 调用接口 - reconciliateBillsApi.manualCheckCreated(data).then(() => { - onCompleteCheck(); - // 清空已选列表 - selectedData.value = []; - selectedRef.value?.clearData(); - }); + await (isBatchMode.value + ? reconciliateBillsApi.manualCheckCreatedBatch({ + paymentIds: paymentIds.join(','), + orders: data, + }) + : reconciliateBillsApi.manualCheckCreated(data)); + onCompleteCheck(); + // 清空已选列表 + selectedData.value = []; + selectedRef.value?.clearData(); } @@ -249,12 +343,22 @@ function onCompleteCheckCreated() { >
-
+
+ +
已选中:{{ multipleCurrentIndex.length }}
+
+
关联sn号:{{ item.relationSn }}
@@ -263,12 +367,19 @@ function onCompleteCheckCreated() {
金额:{{ item.fee }}
支付方式:{{ sysStore.getDictMap('payment', item.type) }}
时间:{{ dayjs(item.ctime).format('YYYY-MM-DD HH:mm:ss') }}
+ +
- +
@@ -320,6 +436,7 @@ function onCompleteCheckCreated() { } :deep(.ant-card-body) { padding: 5px !important; + padding-bottom: 10px !important; } .order-card { width: 100%; diff --git a/apps/finance/src/views/posting/reconciliate/types.d.ts b/apps/finance/src/views/posting/reconciliate/types.d.ts index 124de5a..37bfafa 100644 --- a/apps/finance/src/views/posting/reconciliate/types.d.ts +++ b/apps/finance/src/views/posting/reconciliate/types.d.ts @@ -2,7 +2,8 @@ export interface PaymentRowType { checkoff: number; ctime: string; - fee: string; + fee: number; + orderFlag: number; financeSn: string; id: number; transactionSn: string;