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,100 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Modal, notification, Tag, Upload } from 'ant-design-vue';
import { importBillsApi } from '#/api/posting';
interface Props {
row: any;
type: string;
field: string;
}
const props = withDefaults(defineProps<Props>(), {});
// 定义自定义事件
const emit = defineEmits<{
uploadSuccess: [];
}>();
const uploadRef = ref<any>(null);
function downloadFile(url: string) {
window.open(url);
}
function handleClickUpload() {
if (props.row[props.field]) {
Modal.confirm({
title: '提示',
content: '重传会清空已上传的数据,确认重传吗?',
okText: '确认',
cancelText: '取消',
onOk() {
openFileDialog();
},
});
} else {
openFileDialog();
}
}
function openFileDialog() {
const input = uploadRef.value?.$el?.querySelector('input[type="file"]');
input && input.click();
}
function beforeUpload() {
return true;
}
async function uploadFile(options: any) {
const { file } = options;
// 使用 requestClient.upload 方法,它会自动处理 FormData
await importBillsApi.uploadFile({
file,
year: props.row.year,
month: props.row.month,
type: props.type,
});
notification.success({
message: '上传成功',
});
// message.success(`上传成功大概需要10分钟解析核对数据请耐心等待稍后查询。`);
// 通知父组件刷新列表
emit('uploadSuccess');
}
</script>
<template>
<div>
<span>
<Tag v-if="props.row[props.field]" color="green">已上传</Tag>
<Tag v-else color="orange">未上传</Tag>
</span>
<Tag v-if="props.row[props.field]" class="btn" @click="downloadFile(props.row[props.field])">
下载文件
</Tag>
<Upload
ref="uploadRef"
:before-upload="beforeUpload"
:custom-request="uploadFile"
:show-upload-list="false"
:open-file-dialog-on-click="false"
>
<Tag class="btn" @click="handleClickUpload">
{{ props.row[props.field] ? '点击重传' : '点击上传' }}
</Tag>
</Upload>
</div>
</template>
<style lang="scss" scoped>
.btn {
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,141 @@
<script lang="ts" setup>
import type { VbenFormProps } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import dayjs from 'dayjs';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { importBillsApi } from '#/api/posting';
import FileBox from './components/FileBox.vue';
interface RowType {
year: string;
month: string;
id: string;
wechatFile: string;
alipayFile: string;
bankFile: string;
}
const formOptions: VbenFormProps = {
// 默认展开
collapsed: false,
wrapperClass: 'grid-cols-6',
schema: [
{
component: 'DatePicker',
componentProps: {
allowClear: false,
picker: 'year',
valueFormat: 'YYYY',
},
defaultValue: `${dayjs().year()}`,
fieldName: 'year',
label: '年份',
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: Array.from({ length: 12 }, (_, i) => ({
label: `${i + 1}`,
value: `${i + 1}`,
})),
placeholder: '请选择',
},
fieldName: 'month',
label: '月份',
},
],
// 控制表单是否显示折叠按钮
showCollapseButton: false,
// 是否在字段值改变时提交表单
submitOnChange: true,
// 按下回车时是否提交表单
submitOnEnter: false,
};
const gridOptions: VxeTableGridOptions<RowType> = {
columns: [
{
title: '日期',
slots: {
default: 'date',
},
},
{
field: 'wechatFile',
title: '微信',
slots: {
default: 'wechat-file',
},
},
{
field: 'alipayFile',
title: '支付宝',
slots: {
default: 'alipay-file',
},
},
{
field: 'bankFile',
title: '银行',
slots: {
default: 'bank-file',
},
},
],
exportConfig: {},
height: 'auto',
keepSource: true,
rowConfig: {
isHover: true,
},
pagerConfig: {
enabled: false,
},
proxyConfig: {
response: {
result: 'importList',
list: 'importList',
},
ajax: {
query: async (_, formValues) => {
return await importBillsApi.getImportFileList({
...formValues,
});
},
},
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
// 处理上传成功事件,刷新列表
function handleUploadSuccess() {
gridApi?.query();
}
</script>
<template>
<Page auto-content-height>
<Grid>
<template #date="{ row }"> {{ row.year }}{{ row.month }} </template>
<template #wechat-file="{ row }">
<FileBox :row="row" field="wechatFile" type="0" @upload-success="handleUploadSuccess" />
</template>
<template #alipay-file="{ row }">
<FileBox :row="row" field="alipayFile" type="1" @upload-success="handleUploadSuccess" />
</template>
<template #bank-file="{ row }">
<FileBox :row="row" field="bankFile" type="2" @upload-success="handleUploadSuccess" />
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,208 @@
<script lang="ts" setup>
import type { VbenFormProps } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { watch } from 'vue';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { reconciliateBillsApi } from '#/api/posting';
const props = defineProps({
paymentId: {
type: Number,
default: 0,
},
});
const emit = defineEmits(['completeCheck']);
interface RowType {
id: string;
source: string;
orderSn: string;
tel: string;
fee: string;
type: string;
orderTime: string;
}
const formOptions: VbenFormProps = {
// 默认展开
collapsed: false,
schema: [
{
component: 'RadioGroup',
componentProps: {
optionType: 'button',
options: [
{
label: '金额',
value: '0',
},
],
placeholder: '请选择',
},
defaultValue: '0',
fieldName: 'type',
label: '匹配方式',
},
],
// 控制表单是否显示折叠按钮
showCollapseButton: false,
// 是否在字段值改变时提交表单
submitOnChange: true,
// 按下回车时是否提交表单
submitOnEnter: false,
};
const formatSource = (type: number) => {
switch (type) {
case 0: {
return '一路健康';
}
case 1: {
return '吴门医述';
}
default: {
return type;
}
}
};
const formatType = (type: number) => {
switch (type) {
case 0: {
return '充值';
}
case 1: {
return 'vip';
}
case 2: {
return '课';
}
case 3: {
return '实物';
}
case 4: {
return '培训班';
}
default: {
return type;
}
}
};
const gridOptions: VxeTableGridOptions<RowType> = {
columns: [
{
field: 'source',
title: '来源',
formatter: ({ cellValue }) => {
return formatSource(cellValue);
},
},
{
field: 'orderSn',
title: '订单号',
},
{
field: 'tel',
title: '电话',
},
{
field: 'fee',
title: '金额',
},
{
field: 'type',
title: '订单类型',
formatter: ({ cellValue }) => {
return formatType(cellValue);
},
},
{
field: 'orderTime',
title: '订单时间',
formatter: 'formatDateTime',
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 120,
},
],
exportConfig: {},
height: 'auto',
keepSource: true,
rowConfig: {
isHover: true,
},
pagerConfig: {
enabled: true,
},
proxyConfig: {
response: {
result: 'records',
list: 'records',
total: 'total',
},
ajax: {
query: async ({ page }, formValues) => {
return await reconciliateBillsApi.getOrderList({
page: page.currentPage,
limit: page.pageSize,
paymentId: props.paymentId.toString(),
...formValues,
});
},
},
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
async function onConfirm(row: RowType) {
await reconciliateBillsApi.manualCheckOrder({
paymentId: props.paymentId,
orderId: row.id,
});
emit('completeCheck');
}
// 监听paymentId变化重新加载数据
watch(
() => props.paymentId,
(newPaymentId, oldPaymentId) => {
if (newPaymentId && newPaymentId !== oldPaymentId) {
// 重新加载数据
gridApi?.reload();
}
},
{ immediate: false },
);
</script>
<template>
<div class="orders-container h-full">
<Grid>
<template #action="{ row }">
<Button type="primary" size="small" @click="onConfirm(row)">确认对账</Button>
</template>
</Grid>
</div>
</template>
<style lang="scss" scoped>
.orders-container {
:deep(.vxe-table) {
height: 100% !important;
}
}
</style>

View File

@@ -0,0 +1,176 @@
<script lang="ts" setup>
import type { CreateOrderType } from '../types';
import { computed, ref, watch } from 'vue';
import { Button } from 'ant-design-vue';
import { reconciliateBillsApi } from '#/api/posting/reconciliate';
import { useCardList } from '#/components/card-list/index';
interface RecommendedUser {
tel: string;
}
const props = withDefaults(
defineProps<{
errorItem: CreateOrderType[];
}>(),
{},
);
const emit = defineEmits(['deletedChecked']);
const errorData = ref<any[]>([]);
watch(
() => props.errorItem,
(newVal) => {
errorData.value = newVal.map((item) => ({
id: item.id,
}));
gridApi.setErrorItems(errorData.value);
},
{ deep: true },
);
const gridOptions = computed(() => ({
columns: [
// { field: 'productName', title: '名称' },
{
field: 'orderType',
title: '类型',
formatter: (value: string) => {
const typeMap: Record<string, string> = {
'0': '充值',
'1': 'VIP',
'2': '课程',
'3': '实物',
'4': '培训班',
};
return typeMap[value] || value;
},
},
{
field: 'come',
title: '来源',
formatter: (value: string) => {
const comeMap: Record<string, string> = {
'0': '一路健康',
'1': '吴门医述',
'2': '手动添加',
};
return comeMap[value] || value;
},
},
{
editRender: {
name: 'SelectDropdownRender',
props: {
fetchOptions: (row: CreateOrderType) =>
reconciliateBillsApi
.getRecommendUser({
paymentId: row.paymentId,
come: row.come,
orderType: row.orderType,
courseId: row.courseId,
vipType: row.vipType ?? '',
})
.then(
(data) =>
data.list.map((item: RecommendedUser) => ({
value: item.tel,
})) || [],
),
allowClear: true,
},
},
field: 'tel',
title: '用户手机号',
},
{ editRender: { name: 'Input' }, field: 'orderMoney', title: '订单金额' },
{ editRender: { name: 'Input' }, field: 'realMoney', title: '实际金额' },
{
editRender: {
name: 'DatePicker',
props: {
valueFormat: 'YYYY-MM-DD',
},
},
// 仅VIP 和课程订单 显示
show: (row: CreateOrderType) => row.orderType === '1' || row.orderType === '2',
field: 'startTime',
title: '开始时间',
},
{
editRender: {
name: 'Select',
props: {
options: [
{ label: '半年', value: '6' },
{ label: '一年', value: '12' },
{ label: '两年', value: '24' },
{ label: '三年', value: '36' },
{ label: '四年', value: '48' },
],
},
},
// 仅VIP 和课程订单 显示
show: (row: CreateOrderType) => row.orderType === '1' || row.orderType === '2',
field: 'endTime',
title: '到期时间',
},
],
showTitle: true,
titleField: 'productName',
gridColumns: 4,
gridHeight: '355px',
cardHeight: '345px',
}));
const [Grid, gridApi] = useCardList<CreateOrderType>({
gridOptions: gridOptions.value,
});
// 添加选中项
function handleAdd(rows: CreateOrderType[]) {
gridApi.insertAt(rows);
}
// 卡片删除触发
function handleDelete(row: CreateOrderType) {
emit('deletedChecked', [row.id]);
}
// 删除数据
function deleteData(id: number) {
const index = getData().findIndex((item) => item.id === id);
gridApi.remove(index);
}
// 清空已选
function clearData() {
gridApi.clear();
}
function getData() {
return gridApi.getData();
}
defineExpose({
getData,
handleAdd,
handleDelete,
clearData,
deleteData,
});
</script>
<template>
<Grid>
<template #card-extra="{ row }">
<Button type="link" size="small" danger @click.stop="handleDelete(row)">删除</Button>
</template>
</Grid>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,86 @@
<script lang="ts" setup>
import type { CreateOrderType, PaymentRowType } from '../types';
import { computed } from 'vue';
import { reconciliateBillsApi } from '#/api/posting';
import { useCardListForm } from '../composables/useCardListForm';
import { useCardListGrid } from '../composables/useCardListGrid';
const props = withDefaults(
defineProps<{
payment: PaymentRowType | undefined;
selectedData: CreateOrderType[] | undefined;
tabKey: string;
}>(),
{},
);
const emit = defineEmits(['completeCheck', 'deletedChecked']);
interface RowType {
productId: number;
year: string;
title: string;
}
function transformData(rows: RowType[]) {
return rows.map((row) => ({
id: row.productId,
courseId: '',
tel: '',
come: '2',
orderType: '4',
paymentId: props.payment?.id || '',
productName: row.title,
productId: row.productId,
catalogueId: '',
orderMoney: '',
realMoney: '',
districtMoney: '',
startTime: props.payment?.ctime || '',
endTime: '',
}));
}
const { formOptions } = useCardListForm({
schema: [
{
component: 'Input',
componentProps: { allowClear: true },
fieldName: 'title',
defaultValue: '',
label: '培训班名称',
},
],
});
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
payment: computed(() => props.payment),
selectedData: computed(() => props.selectedData),
rowKey: 'productId',
rowKeyPrefix: props.tabKey,
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'title', title: '培训班名称', minWidth: 200 },
{ field: 'year', title: '年份', minWidth: 100 },
],
query: (params) =>
reconciliateBillsApi.getTrainingClassList({
...params,
paymentId: props.payment?.id || 0,
}),
transformData,
emit,
formOptions,
});
defineExpose({ cancelCheck, setChecked });
</script>
<template>
<div class="orders-container h-full">
<Grid />
</div>
</template>

View File

@@ -0,0 +1,76 @@
<script lang="ts" setup>
import type { CreateOrderType, PaymentRowType } from '../types';
import { computed } from 'vue';
import { reconciliateBillsApi } from '#/api/posting';
import { useCardListGrid } from '../composables/useCardListGrid';
const props = withDefaults(
defineProps<{
payment: PaymentRowType | undefined;
selectedData: CreateOrderType[] | undefined;
tabKey: string;
}>(),
{},
);
const emit = defineEmits(['completeCheck', 'deletedChecked']);
interface RowType {
type: string;
title: string;
}
function transformData(rows: RowType[]) {
return rows.map((row) => ({
id: row.type,
courseId: '',
tel: '',
come: '2',
orderType: '1',
paymentId: props.payment?.id || '',
productName: row.title,
productId: '',
catalogueId: '',
orderMoney: '',
realMoney: '',
districtMoney: '',
startTime: props.payment?.ctime || '',
endTime: '',
vipType: row.type,
}));
}
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
payment: computed(() => props.payment),
selectedData: computed(() => props.selectedData),
rowKey: 'type',
rowKeyPrefix: props.tabKey,
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'title', title: 'VIP名称', minWidth: 150 },
],
query: (params) =>
reconciliateBillsApi.getVipList({
...params,
paymentId: props.payment?.id || 0,
}),
transformData,
emit,
gridOptions: {
pagerConfig: {
enabled: false,
},
},
});
defineExpose({ cancelCheck, setChecked });
</script>
<template>
<div class="orders-container h-full">
<Grid />
</div>
</template>

View File

@@ -0,0 +1,83 @@
<script lang="ts" setup>
import type { CreateOrderType, PaymentRowType } from '../types';
import { computed } from 'vue';
import { reconciliateBillsApi } from '#/api/posting';
import { useCardListForm } from '../composables/useCardListForm';
import { useCardListGrid } from '../composables/useCardListGrid';
const props = withDefaults(
defineProps<{
payment: PaymentRowType | undefined;
selectedData: CreateOrderType[] | undefined;
tabKey: string;
}>(),
{},
);
const emit = defineEmits(['completeCheck', 'deletedChecked']);
interface RowType {
courseId: number;
productId: number;
price: number;
productName: string;
}
function transformData(rows: RowType[]) {
return rows.map((row) => ({
id: row.productId,
courseId: row.courseId || '',
tel: '',
come: '1',
orderType: '2',
paymentId: props.payment?.id || '',
productName: row.productName,
productId: row.productId,
catalogueId: '',
orderMoney: String(row.price),
realMoney: '',
districtMoney: '',
startTime: props.payment?.ctime || '',
endTime: '',
}));
}
const { formOptions } = useCardListForm({
schema: [
{
component: 'Input',
componentProps: { allowClear: true },
fieldName: 'title',
defaultValue: '',
label: '商品名称',
},
],
});
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
payment: computed(() => props.payment),
selectedData: computed(() => props.selectedData),
rowKey: 'productId',
rowKeyPrefix: props.tabKey,
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'productName', title: '商品名称', minWidth: 100 },
{ field: 'price', title: '金额', minWidth: 100 },
],
query: reconciliateBillsApi.getWumenCourseBuyList,
transformData,
emit,
formOptions,
});
defineExpose({ cancelCheck, setChecked });
</script>
<template>
<div class="orders-container h-full">
<Grid />
</div>
</template>

View File

@@ -0,0 +1,74 @@
<script lang="ts" setup>
import type { CreateOrderType, PaymentRowType } from '../types';
import { computed } from 'vue';
import { reconciliateBillsApi } from '#/api/posting';
import { useCardListGrid } from '../composables/useCardListGrid';
const props = withDefaults(
defineProps<{
payment: PaymentRowType | undefined;
selectedData: CreateOrderType[] | undefined;
tabKey: string;
}>(),
{},
);
const emit = defineEmits(['completeCheck', 'deletedChecked']);
interface RowType {
id: number;
point: number;
tel: string;
note: string;
}
function transformData(rows: RowType[]) {
return rows.map((row) => ({
id: row.id,
courseId: '',
tel: row.tel,
come: '1',
orderType: '0',
paymentId: props.payment?.id || '',
productName: row.note,
productId: '',
catalogueId: '',
orderMoney: String(row.point),
realMoney: '',
districtMoney: '',
startTime: '',
endTime: '',
}));
}
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
payment: computed(() => props.payment),
selectedData: computed(() => props.selectedData),
rowKey: 'id',
rowKeyPrefix: props.tabKey,
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'note', title: '备注', minWidth: 150 },
{ field: 'point', title: '积分', minWidth: 100 },
{ field: 'tel', title: '手机号', minWidth: 150 },
],
query: (params) =>
reconciliateBillsApi.getWumenPointBuyList({
...params,
paymentId: props.payment?.id || 0,
}),
transformData,
emit,
});
defineExpose({ cancelCheck, setChecked });
</script>
<template>
<div class="orders-container h-full">
<Grid />
</div>
</template>

View File

@@ -0,0 +1,77 @@
<script lang="ts" setup>
import type { CreateOrderType, PaymentRowType } from '../types';
import { computed } from 'vue';
import { reconciliateBillsApi } from '#/api/posting';
import { useCardListGrid } from '../composables/useCardListGrid';
const props = withDefaults(
defineProps<{
payment: PaymentRowType | undefined;
selectedData: CreateOrderType[] | undefined;
tabKey: string;
}>(),
{},
);
const emit = defineEmits(['completeCheck', 'deletedChecked']);
interface RowType {
ucbId: number;
productId: number;
courseId: number;
catalogueId: number;
productName: string;
price: number;
tel: string;
}
function transformData(rows: RowType[]) {
return rows.map((row) => ({
id: row.ucbId,
paymentId: props.payment?.id || 0,
courseId: row.courseId,
tel: row.tel,
come: '1',
orderType: '2',
productName: row.productName,
productId: '',
catalogueId: row.catalogueId,
orderMoney: '',
realMoney: '',
districtMoney: '',
startTime: props.payment?.ctime || '',
endTime: '',
}));
}
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
payment: computed(() => props.payment),
selectedData: computed(() => props.selectedData),
rowKey: 'ucbId',
rowKeyPrefix: props.tabKey,
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'productName', title: '课程名称', minWidth: 100 },
{ field: 'price', title: '金额', minWidth: 100 },
{ field: 'tel', title: '手机号', minWidth: 150 },
],
query: (params) =>
reconciliateBillsApi.getWumenRecommendCourseBuyList({
...params,
paymentId: props.payment?.id || 0,
}),
transformData,
emit,
});
defineExpose({ cancelCheck, setChecked });
</script>
<template>
<div class="orders-container h-full">
<Grid />
</div>
</template>

View File

@@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { CreateOrderType, PaymentRowType } from '../types';
import { computed } from 'vue';
import { reconciliateBillsApi } from '#/api/posting';
import { useCardListForm } from '../composables/useCardListForm';
import { useCardListGrid } from '../composables/useCardListGrid';
const props = withDefaults(
defineProps<{
payment: PaymentRowType | undefined;
selectedData: CreateOrderType[] | undefined;
tabKey: string;
}>(),
{},
);
const emit = defineEmits(['completeCheck', 'deletedChecked']);
interface RowType {
id: number;
price: number;
productName: string;
}
function transformData(rows: RowType[]) {
return rows.map((row) => ({
id: row.id,
courseId: '',
tel: '',
come: '1',
orderType: '3',
paymentId: props.payment?.id || '',
productName: row.productName,
productId: row.id,
catalogueId: '',
orderMoney: String(row.price),
realMoney: '',
districtMoney: '',
startTime: '',
endTime: '',
}));
}
const { formOptions } = useCardListForm({
schema: [
{
component: 'Input',
componentProps: { allowClear: true },
fieldName: 'productName',
defaultValue: '',
label: '商品名称',
},
],
});
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
payment: computed(() => props.payment),
selectedData: computed(() => props.selectedData),
rowKey: 'id',
rowKeyPrefix: props.tabKey,
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'productName', title: '商品名称', minWidth: 150 },
{ field: 'price', title: '金额', minWidth: 100 },
],
query: reconciliateBillsApi.getWumenPhysicalBuyList,
transformData,
emit,
formOptions,
});
defineExpose({ cancelCheck, setChecked });
</script>
<template>
<div class="orders-container h-full">
<Grid />
</div>
</template>

View File

@@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { CreateOrderType, PaymentRowType } from '../types';
import { computed } from 'vue';
import { reconciliateBillsApi } from '#/api/posting';
import { useCardListForm } from '../composables/useCardListForm';
import { useCardListGrid } from '../composables/useCardListGrid';
const props = withDefaults(
defineProps<{
payment: PaymentRowType | undefined;
selectedData: CreateOrderType[] | undefined;
tabKey: string;
}>(),
{},
);
const emit = defineEmits(['completeCheck', 'deletedChecked']);
interface RowType {
price: string;
courseId: string;
productName: string;
}
function transformData(rows: RowType[]) {
return rows.map((row) => ({
id: row.courseId,
courseId: row.courseId || '',
tel: '',
come: '0',
orderType: '2',
paymentId: props.payment?.id || '',
productName: row.productName,
productId: '',
catalogueId: '',
orderMoney: '',
realMoney: '',
districtMoney: '',
startTime: props.payment?.ctime || '',
endTime: '',
}));
}
const { formOptions } = useCardListForm({
schema: [
{
component: 'Input',
componentProps: { allowClear: true },
fieldName: 'title',
defaultValue: '',
label: '商品名称',
},
],
});
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
payment: computed(() => props.payment),
selectedData: computed(() => props.selectedData),
rowKey: 'courseId',
rowKeyPrefix: props.tabKey,
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'productName', title: '商品名称', minWidth: 150 },
{ field: 'price', title: '金额', minWidth: 100 },
],
query: reconciliateBillsApi.getYiluCourseBuyList,
transformData,
emit,
formOptions,
});
defineExpose({ cancelCheck, setChecked });
</script>
<template>
<div class="orders-container h-full">
<Grid />
</div>
</template>

View File

@@ -0,0 +1,74 @@
<script lang="ts" setup>
import type { CreateOrderType, PaymentRowType } from '../types';
import { computed } from 'vue';
import { reconciliateBillsApi } from '#/api/posting';
import { useCardListGrid } from '../composables/useCardListGrid';
const props = withDefaults(
defineProps<{
payment: PaymentRowType | undefined;
selectedData: CreateOrderType[] | undefined;
tabKey: string;
}>(),
{},
);
const emit = defineEmits(['completeCheck', 'deletedChecked']);
interface RowType {
id: number;
point: number;
tel: string;
note: string;
}
function transformData(rows: RowType[]) {
return rows.map((row) => ({
id: row.id,
courseId: '',
tel: row.tel,
come: '0',
orderType: '0',
paymentId: props.payment?.id || '',
productName: row.note,
productId: '',
catalogueId: '',
orderMoney: String(row.point),
realMoney: '',
districtMoney: '',
startTime: props.payment?.ctime || '',
endTime: '',
}));
}
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
payment: computed(() => props.payment),
selectedData: computed(() => props.selectedData),
rowKey: 'id',
rowKeyPrefix: props.tabKey,
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'note', title: '备注', minWidth: 150 },
{ field: 'point', title: '充值金额', minWidth: 100 },
{ field: 'tel', title: '手机号', minWidth: 150 },
],
query: (params) =>
reconciliateBillsApi.getYiluPointBuyList({
...params,
paymentId: props.payment?.id || 0,
}),
transformData,
emit,
});
defineExpose({ cancelCheck, setChecked });
</script>
<template>
<div class="orders-container h-full">
<Grid />
</div>
</template>

View File

@@ -0,0 +1,77 @@
<script lang="ts" setup>
import type { CreateOrderType, PaymentRowType } from '../types';
import { computed } from 'vue';
import { reconciliateBillsApi } from '#/api/posting';
import { useCardListGrid } from '../composables/useCardListGrid';
const props = withDefaults(
defineProps<{
payment: PaymentRowType | undefined;
selectedData: CreateOrderType[] | undefined;
tabKey: string;
}>(),
{},
);
const emit = defineEmits(['completeCheck', 'deletedChecked']);
interface RowType {
courseFee: string;
courseId: string;
studyDays: number;
cellPhone: string;
productName: string;
createDate: string;
}
function transformData(rows: RowType[]) {
return rows.map((row) => ({
id: row.courseId,
paymentId: props.payment?.id || 0,
courseId: row.courseId,
tel: row.cellPhone,
come: '0',
orderType: '2',
productName: row.productName,
productId: '',
catalogueId: '',
orderMoney: '',
realMoney: '',
districtMoney: '',
startTime: row.createDate,
endTime: String(row.studyDays),
}));
}
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
payment: computed(() => props.payment),
selectedData: computed(() => props.selectedData),
rowKey: 'courseId',
rowKeyPrefix: props.tabKey,
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'productName', title: '课程名称', minWidth: 150 },
{ field: 'courseFee', title: '金额', minWidth: 100 },
{ field: 'cellPhone', title: '手机号', minWidth: 150 },
{ field: 'studyDays', title: '学习天数', minWidth: 100 },
],
query: (params) =>
reconciliateBillsApi.getYiluRecommendCourseBuyList({
...params,
paymentId: props.payment?.id || 0,
}),
transformData,
emit,
});
defineExpose({ cancelCheck, setChecked });
</script>
<template>
<div class="orders-container h-full">
<Grid />
</div>
</template>

View File

@@ -0,0 +1,86 @@
<script lang="ts" setup>
import type { CreateOrderType, PaymentRowType } from '../types';
import { computed } from 'vue';
import { reconciliateBillsApi } from '#/api/posting';
import { useCardListForm } from '../composables/useCardListForm';
import { useCardListGrid } from '../composables/useCardListGrid';
const props = withDefaults(
defineProps<{
payment: PaymentRowType | undefined;
selectedData: CreateOrderType[] | undefined;
tabKey: string;
}>(),
{},
);
const emit = defineEmits(['completeCheck', 'deletedChecked']);
interface RowType {
id: string;
price: number;
productName: string;
}
function transformData(rows: RowType[]) {
return rows.map((row) => ({
id: row.id,
courseId: '',
tel: '',
come: '0',
orderType: '3',
paymentId: props.payment?.id || '',
productName: row.productName,
productId: row.id,
catalogueId: '',
orderMoney: String(row.price),
realMoney: '',
districtMoney: '',
startTime: props.payment?.ctime || '',
endTime: '',
}));
}
const { formOptions } = useCardListForm({
schema: [
{
component: 'Input',
componentProps: { allowClear: true },
fieldName: 'productName',
defaultValue: '',
label: '商品名称',
},
],
});
const { Grid, cancelCheck, setChecked } = useCardListGrid<RowType, CreateOrderType>({
payment: computed(() => props.payment),
selectedData: computed(() => props.selectedData),
rowKey: 'id',
rowKeyPrefix: props.tabKey,
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'productName', title: '商品名称', minWidth: 150 },
{ field: 'price', title: '金额', minWidth: 100 },
],
query: (params) =>
reconciliateBillsApi.getPhysicalBuyList({
...params,
paymentId: props.payment?.id || 0,
}),
transformData,
emit,
formOptions,
});
defineExpose({ cancelCheck, setChecked });
</script>
<template>
<div class="orders-container h-full">
<Grid />
</div>
</template>

View File

@@ -0,0 +1,18 @@
import type { VbenFormProps } from '#/adapter/form';
export function useCardListForm(userFormOptions?: VbenFormProps) {
const defaultFormOptions: VbenFormProps = {
collapsed: false,
wrapperClass: 'grid-cols-4',
showCollapseButton: false,
submitOnChange: true,
submitOnEnter: false,
};
const formOptions: VbenFormProps = {
...defaultFormOptions,
...userFormOptions,
};
return { formOptions };
}

View File

@@ -0,0 +1,159 @@
import type { Ref } from 'vue';
import type { VxeGridListeners, VxeTableGridOptions } from '#/adapter/vxe-table';
import { watch } from 'vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
interface UseCardListGridOptions<RowType, OrderType> {
payment: Ref<any>;
selectedData: Ref<OrderType[] | undefined>;
rowKey: string;
rowKeyPrefix?: string;
columns: VxeTableGridOptions<RowType>['columns'];
query: (params: any) => Promise<any>;
transformData: (row: RowType[]) => OrderType[];
emit: (event: 'completeCheck' | 'deletedChecked', payload: any) => void;
formOptions?: any;
gridOptions?: Partial<VxeTableGridOptions<RowType>>;
gridEvents?: VxeGridListeners<RowType>;
}
export function useCardListGrid<RowType, OrderType>(
options: UseCardListGridOptions<RowType, OrderType>,
) {
const {
payment,
selectedData,
rowKey,
rowKeyPrefix,
columns,
query,
transformData,
emit,
formOptions,
gridOptions: userGridOptions,
gridEvents: userGridEvents,
} = options;
function setChecked(data: OrderType[]) {
const currentPageData = gridApi.grid.getData();
const rows = currentPageData.filter((row: any) =>
data.some((item: any) => item.id === row[rowKey]),
);
rows.length > 0 && gridApi.grid.setCheckboxRow(rows, true);
}
function cancelCheck(id: number) {
gridApi.grid.setCheckboxRow({ [rowKey]: id }, false);
}
const defaultGridOptions: VxeTableGridOptions<RowType> = {
columns,
exportConfig: {},
height: 'auto',
keepSource: true,
rowConfig: {
keyField: rowKey,
isHover: true,
},
checkboxConfig: {
reserve: true,
showReserveStatus: true,
highlight: true,
trigger: 'row',
},
pagerConfig: {
enabled: true,
},
proxyConfig: {
response: {
result: 'records',
total: 'total',
list: 'records',
},
ajax: {
query: async ({ page }, formValues) => {
const data = await query({
page: page.currentPage,
limit: page.pageSize,
...formValues,
});
// 确保 listKey 是字符串类型
const responseList = userGridOptions?.proxyConfig?.response?.list;
const listKey = typeof responseList === 'string' ? responseList : 'records';
const list = data[listKey].map((item: any) => ({
...item,
[rowKey]: `${rowKeyPrefix || ''}${item[rowKey]}`,
}));
data[listKey] = list;
return data;
},
querySuccess: () => {
setChecked(selectedData.value || []);
},
},
},
};
const finalGridOptions: VxeTableGridOptions<RowType> = {
...defaultGridOptions,
...userGridOptions,
// 对 proxyConfig 进行特殊处理,确保用户配置能覆盖默认配置
proxyConfig: {
...defaultGridOptions.proxyConfig,
...userGridOptions?.proxyConfig,
// 对 proxyConfig.ajax 进行特殊处理
ajax: {
...defaultGridOptions.proxyConfig?.ajax,
...userGridOptions?.proxyConfig?.ajax,
},
// 对 proxyConfig.response 进行特殊处理
response: {
...defaultGridOptions.proxyConfig?.response,
...userGridOptions?.proxyConfig?.response,
},
},
};
const defaultGridEvents: VxeGridListeners<RowType> = {
checkboxChange: ({ checked, row }: any) => {
checked ? emit('completeCheck', transformData([row])) : emit('deletedChecked', [row[rowKey]]);
},
checkboxAll: ({ checked }: any) => {
const rows = gridApi.grid.getData();
const ids = rows.map((item: any) => item[rowKey]);
checked ? emit('completeCheck', transformData(rows)) : emit('deletedChecked', ids);
},
};
const finalGridEvents: VxeGridListeners<RowType> = {
...defaultGridEvents,
...userGridEvents,
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions: finalGridOptions as any,
gridEvents: finalGridEvents,
});
watch(
payment,
(newPayment, oldPayment) => {
if (newPayment?.id && newPayment.id !== oldPayment?.id) {
gridApi?.reload();
}
},
{ immediate: false },
);
return {
Grid,
gridApi,
cancelCheck,
setChecked,
};
}

View File

@@ -0,0 +1,229 @@
<script lang="ts" setup>
import type { PaymentRowType } from './types';
import type { VbenFormProps } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { ref, watch } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, message, notification, Tag } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { reconciliateBillsApi } from '#/api/posting';
import { useSysStore } from '#/store';
import Manual from './modules/Manual.vue';
const sysStore = useSysStore();
const formOptions: VbenFormProps = {
// 默认展开
collapsed: false,
// 设置每行最多显示4个表单项
wrapperClass: 'grid-cols-6',
schema: [
{
component: 'DatePicker',
componentProps: {
allowClear: false,
picker: 'month',
valueFormat: 'YYYY-MM',
},
defaultValue: dayjs().format('YYYY-MM'),
fieldName: 'date',
label: '日期',
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: sysStore.getDictList('payment'),
placeholder: '请选择',
},
fieldName: 'type',
label: '支付方式',
},
{
component: 'RadioGroup',
componentProps: {
optionType: 'button',
options: [
{
label: '全部',
value: '',
},
...sysStore.getDictList('checkoff'),
],
placeholder: '请选择',
},
defaultValue: '',
fieldName: 'checkoff',
label: '核对状态',
// 核对状态占1列
formItemClass: 'col-span-2',
},
],
// 控制表单是否显示折叠按钮
showCollapseButton: false,
// 是否在字段值改变时提交表单
submitOnChange: true,
// 按下回车时是否提交表单
submitOnEnter: false,
};
const gridOptions: VxeTableGridOptions<PaymentRowType> = {
columns: [
{ type: 'checkbox', width: 60 },
{
field: 'type',
title: '支付方式',
minWidth: 80,
formatter: ({ cellValue }) => {
return sysStore.getDictMap('payment', cellValue);
},
},
{ field: 'fee', title: '金额', minWidth: 100 },
{ field: 'ctime', title: '时间', minWidth: 100, formatter: 'formatDateTime' },
{ field: 'transactionSn', title: '业务流水号', minWidth: 150 },
{ field: 'financeSn', title: '财务流水号', minWidth: 150 },
{ field: 'relationSn', title: '关联sn号', minWidth: 160 },
{ field: 'remark', title: '备注', minWidth: 120 },
{
field: 'checkoff',
title: '状态',
minWidth: 100,
slots: { default: 'status' },
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 120,
},
],
exportConfig: {},
height: 'auto',
keepSource: true,
rowConfig: {
isHover: true,
},
pagerConfig: {
enabled: true,
},
proxyConfig: {
response: {
result: 'records',
total: 'total',
list: 'records',
},
ajax: {
query: async ({ page }, formValues) => {
const date = formValues.date.split('-');
delete formValues.date;
return await reconciliateBillsApi.getReconciliationList({
page: page.currentPage,
limit: page.pageSize,
year: date[0],
month: date[1],
...formValues,
});
},
},
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
async function onStartSysCheck() {
// 获取表单值
const formValues = await gridApi?.formApi?.getValues();
// 处理日期格式
if (formValues?.date) {
const date = formValues.date.split('-');
const params = {
year: date[0],
month: date[1],
};
// 调用自动核对接口
const hide = message.loading('系统自动核对中...', 0);
await reconciliateBillsApi.autoCheck(params);
hide();
gridApi?.query();
notification.success({
message: '完成',
description: '系统自动核对完成,核对失败的单据请人工核对。',
});
}
}
const selectData = ref<PaymentRowType[]>([]);
const manualDialogVisible = ref(false);
// 监听弹框关闭,刷新列表数据
watch(manualDialogVisible, (newVal) => {
if (!newVal) {
// 弹框关闭时刷新列表(保持在当前页)
gridApi?.query();
}
});
async function onManualCheck(data?: PaymentRowType) {
// 如果没有传入数据,则获取选中的行
if (data) {
selectData.value = [data];
} else {
const selectedRows = await gridApi?.grid?.getCheckboxRecords();
if (selectedRows.length === 0) {
message.error('请先选择数据');
return;
}
selectData.value = selectedRows;
}
// 处理selectDatacheckoff状态为1才需要人工核对
const originalLength = selectData.value.length;
selectData.value = selectData.value.filter((item: PaymentRowType) => item.checkoff === 1);
const filteredCount = originalLength - selectData.value.length;
// 如果有数据被过滤掉,显示警告
if (filteredCount > 0) {
message.warning(`已过滤掉 ${filteredCount} 条不符合条件的数据`);
}
if (selectData.value.length === 0) {
message.error('选择的数据中没有可进行人工对账的数据');
} else {
manualDialogVisible.value = true;
}
}
</script>
<template>
<Page auto-content-height>
<Grid>
<template #toolbar-actions>
<div class="flex gap-2">
<Button type="primary" @click="onStartSysCheck()">启动系统自动对账</Button>
<Button type="primary" @click="onManualCheck()">人工对账</Button>
</div>
</template>
<template #action="{ row }">
<Button v-if="row.checkoff === 1" type="primary" size="small" @click="onManualCheck(row)">
人工对账
</Button>
</template>
<template #status="{ row }">
<Tag :color="sysStore.getDictByList('checkoff', row.checkoff).color || 'info'">
{{ sysStore.getDictMap('checkoff', row.checkoff) }}
</Tag>
</template>
</Grid>
<Manual v-if="manualDialogVisible" v-model:show="manualDialogVisible" :data="selectData" />
</Page>
</template>

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>

View File

@@ -0,0 +1,29 @@
// 实际支付记录
export interface PaymentRowType {
checkoff: number;
ctime: string;
fee: string;
financeSn: string;
id: number;
transactionSn: string;
relationSn: string;
type: number;
}
// 创建订单参数
export interface CreateOrderType {
id: number | string;
paymentId: number | string;
courseId: number | string;
tel: string;
come: string;
orderType: string;
productName: string;
catalogueId: number | string;
orderMoney: string;
districtMoney: string;
realMoney: string;
startTime: string;
endTime: string;
vipType?: string;
}