feat(财务统计): 新增月度财务报表功能

添加月度财务报表页面及相关工具函数和类型定义
修改统计接口参数类型以支持字符串格式的月份
调整空状态显示样式
This commit is contained in:
2026-02-04 16:04:03 +08:00
parent 1468891bf5
commit 9edad91dca
6 changed files with 739 additions and 11 deletions

View File

@@ -9,7 +9,7 @@ export const statisticsApi = {
* @param data.year 年份
* @returns 天医币报表数据
*/
getReportTianyibi: (data: { month?: string; year: number }) => {
getReportTianyibi: (data: { month: string; year: number | string }) => {
return requestClient.post('common/statistics/pointStatistics', data);
},
@@ -20,7 +20,7 @@ export const statisticsApi = {
* @param data.year 年份
* @returns 实物报表数据
*/
getPhysicalStatistics: (data: { month?: string; year: number }) => {
getPhysicalStatistics: (data: { month: string; year: number | string }) => {
return requestClient.post('common/statistics/physicalStatistics', data);
},
@@ -31,7 +31,7 @@ export const statisticsApi = {
* @param data.year 年份
* @returns 培训班报表数据
*/
getTrainingClassStatistics: (data: { month?: string; year: number }) => {
getTrainingClassStatistics: (data: { month: string; year: number | string }) => {
return requestClient.post('common/statistics/trainingClassStatistics', data);
},
@@ -42,7 +42,7 @@ export const statisticsApi = {
* @param data.year 年份
* @returns VIP报表数据
*/
getVipStatistics: (data: { month?: string; year: number }) => {
getVipStatistics: (data: { month: string; year: number | string }) => {
return requestClient.post('common/statistics/vipStatistics', data);
},
@@ -53,7 +53,7 @@ export const statisticsApi = {
* @param data.year 年份
* @returns 课程报表数据
*/
getCourseStatistics: (data: { month?: string; year: number }) => {
getCourseStatistics: (data: { month: string; year: number | string }) => {
return requestClient.post('common/statistics/courseStatistics', data);
},
@@ -64,7 +64,7 @@ export const statisticsApi = {
* @param data.year 年份
* @returns 天医币报表数据
*/
downloadReportTianyibi: (data: { month?: string; year: number }) => {
downloadReportTianyibi: (data: { month: string; year: number | string }) => {
return defaultRequestClient.download<Blob>('common/statistics/pointInfoExport', {
data,
});
@@ -76,7 +76,7 @@ export const statisticsApi = {
* @param data.year 年份
* @returns 实物报表数据
*/
downloadReportPhysical: (data: { month?: string; year: number }) => {
downloadReportPhysical: (data: { month: string; year: number | string }) => {
return defaultRequestClient.download<Blob>('common/statistics/physicalInfoExport', {
data,
});
@@ -88,7 +88,7 @@ export const statisticsApi = {
* @param data.year 年份
* @returns 培训班报表数据
*/
downloadReportTrainingClass: (data: { month?: string; year: number }) => {
downloadReportTrainingClass: (data: { month: string; year: number | string }) => {
return defaultRequestClient.download<Blob>('common/statistics/trainingClassInfoExport', {
data,
});
@@ -100,7 +100,7 @@ export const statisticsApi = {
* @param data.year 年份
* @returns VIP报表数据
*/
downloadReportVip: (data: { month?: string; year: number }) => {
downloadReportVip: (data: { month: string; year: number | string }) => {
return defaultRequestClient.download<Blob>('common/statistics/vipInfoExport', {
data,
});
@@ -112,9 +112,31 @@ export const statisticsApi = {
* @param data.year 年份
* @returns 课程报表数据
*/
downloadReportCourse: (data: { month?: string; year: number }) => {
downloadReportCourse: (data: { month: string; year: number | string }) => {
return defaultRequestClient.download<Blob>('common/statistics/courseInfoExport', {
data,
});
},
/**
* 获取月总结
* @param data 月份 格式为YYYY-MM
* @returns 月总结数据
*/
getSumMonthStatistics: (data: string) => {
return requestClient.post('common/statistics/getStatistics', { orderTime: data });
},
/**
* 汇总导出
* @param data 月份 格式为YYYY-MM
* @param data.month 月份
* @param data.year 年份
* @returns 月总结数据
*/
downloadSumMonthStatistics: (data: { month: string; year: number | string }) => {
return defaultRequestClient.download<Blob>('common/statistics/statisticsInfoExport', {
data,
});
},
};

View File

@@ -56,6 +56,15 @@ const routes: RouteRecordRaw[] = [
path: '/statistics/course-report',
component: () => import('#/views/statistics/course/report.vue'),
},
{
meta: {
title: '月度财务报表',
keepAlive: true,
},
name: 'FinanceMonthReport',
path: '/statistics/finance-month-report',
component: () => import('#/views/statistics/summary-month/report.vue'),
},
],
},
];

View File

@@ -81,7 +81,7 @@ const handleDownloadMonth = async (index: number) => {
<div class="content relative min-h-2 flex-1 px-3 py-4">
<div
v-if="list.length === 0 && !loading"
class="col-span-3 flex items-center justify-center"
class="col-span-3 flex h-full items-center justify-center"
>
<Empty />
</div>

View File

@@ -0,0 +1,127 @@
import type { IncomeItem, VipAndCourseMonthStatistics } from './types';
// 处理收款汇总数据以及实物和培训班数据
export function handleSumAndNoNeedAmortizateMonthData(
responseData: any[],
options: {
childDictArr: { title: string; type: string }[];
typeDictArr: string[];
typeKey: string;
},
) {
let sumMonthTotal = 0;
const resultData = [] as any[];
const { childDictArr, typeDictArr, typeKey } = options;
typeDictArr.forEach((type) => {
// 查找接口返回数据中是否有当前支付平台的数据
const index = responseData.findIndex((item) => item[typeKey] === type);
// 取得当前支付平台的数据项,若不存在则用空对象代替
const resItem = index === -1 ? {} : responseData[index];
// 组装页面渲染需要的数据格式
resultData.push({
type,
sumFee: resItem.sumFee || 0,
children: childDictArr.map((childType) => ({
title: childType.title,
fee: resItem[childType.type] || 0,
})),
});
// 累加总金额
// 使用 toFixed 避免精度丢失
sumMonthTotal = Number.parseFloat((sumMonthTotal + (resItem.sumFee || 0)).toFixed(3));
});
return {
sumMonthTotal,
resultData,
};
}
// 获取当前月份和上一个月份的数据
async function getTwoMonthData(
requestMethod: (params: { month: string; year: number | string }) => Promise<any>,
date: string,
) {
// 处理日期格式将YYYY-MM转换为year: YYYY, month: MM
const [year = '', month = ''] = date.split('-');
const params = {
year,
month,
};
// 处理跨年情况
const lastMonthParams = {
year: params.month === '01' ? Number(params.year) - 1 : params.year,
month: params.month === '01' ? '12' : (Number(params.month) - 1).toString().padStart(2, '0'),
};
// 调用接口 查询当前月份数据和上一个月数据
// 同时发送两次请求
const [currentMonthData, lastMonthData] = await Promise.all([
requestMethod(params),
requestMethod(lastMonthParams),
]);
return {
currentMonthData,
lastMonthData,
};
}
// 处理VIP、课程数据的公共方法
export async function handleVipAndCourseMonthData(
requestMethod: (params: {
month: string;
year: number | string;
}) => Promise<VipAndCourseMonthStatistics>,
date: string,
incomeTypes: string[],
) {
const { currentMonthData, lastMonthData } = await getTwoMonthData(requestMethod, date);
return {
...currentMonthData,
incomes: incomeTypes.map((type) => ({
type,
fee: currentMonthData.incomes.find((item: IncomeItem) => item.type === type)?.fee || 0,
})),
lastNotyet: lastMonthData?.notyet || 0,
incomesTotal: Number.parseFloat(
currentMonthData.incomes
.reduce((acc: number, cur: IncomeItem) => acc + cur.fee, 0)
.toFixed(3),
),
};
}
// 处理天医币明细数据
export async function handleTianyibiDetailData(
requestMethod: (params: {
month: string;
year: number | string;
}) => Promise<VipAndCourseMonthStatistics>,
date: string,
incomeTypes: string[],
consumesTypes: string[],
) {
const { currentMonthData, lastMonthData } = await getTwoMonthData(requestMethod, date);
return {
...currentMonthData.map,
incomes: incomeTypes.map((type) => ({
type,
fee: currentMonthData.map.incomes.find((item: IncomeItem) => item?.type === type)?.fee || 0,
point:
currentMonthData.map.incomes.find((item: IncomeItem) => item?.type === type)?.point || 0,
})),
consumes: consumesTypes.map((type) => ({
type,
fee: currentMonthData.map.consumes.find((item: IncomeItem) => item?.type === type)?.fee || 0,
})),
lastSurplus: lastMonthData?.map.surplus || 0,
incomesTotal: Number.parseFloat(
currentMonthData.map.incomes
.reduce((acc: number, cur: IncomeItem) => acc + cur.fee, 0)
.toFixed(3),
),
consumesTotal: Number.parseFloat(
currentMonthData.map.consumes
.reduce((acc: number, cur: IncomeItem) => acc + cur.fee, 0)
.toFixed(3),
),
};
}

View File

@@ -0,0 +1,530 @@
<script lang="ts" setup>
import type { Dayjs } from 'dayjs';
import type {
IncomeItem,
NoNeedAmortizateMonthStatistics,
SumAndNoNeedAmortizateMonthDataItem,
SumMonthStatistics,
} from './types';
import { computed, onMounted, reactive, ref } from 'vue';
import { Loading, Page } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, Card, DatePicker, message } from 'ant-design-vue';
import dayjs from 'dayjs';
import { statisticsApi } from '#/api/statistics';
import { useSysStore } from '#/store/sys';
import {
handleSumAndNoNeedAmortizateMonthData,
handleTianyibiDetailData,
handleVipAndCourseMonthData,
} from './report-utils';
const sysStore = useSysStore();
const loading = ref(false);
const date = ref(dayjs().format('YYYY-MM'));
const disabledDate = (date: Dayjs) => {
const currentYear = dayjs().year();
const currentMonth = dayjs().month();
// 如果是当前年份,超过本月的月份不能选
if (date.year() === currentYear) {
return date.month() > currentMonth;
}
// 如果是未来年份,所有月份都不能选
if (date.year() > currentYear) {
return true;
}
// 过去年份的所有月份都可以选
return false;
};
const platList = computed(() => sysStore.getDictList('payment'));
const loadingText = '数据加载中...';
const loadingError = '数据加载或解析失败';
// 获取收款汇总数据和实物和培训班数据
const sumAndNoNeedAmortizateMonthData = reactive({
paymentSummary: [] as SumMonthStatistics[],
physicalAndTraining: [] as NoNeedAmortizateMonthStatistics[],
});
async function getSumAndNoNeedAmortizateMonth() {
const res = await statisticsApi.getSumMonthStatistics(date.value);
sumAndNoNeedAmortizateMonthData.paymentSummary = res.paymentSummary || [];
sumAndNoNeedAmortizateMonthData.physicalAndTraining = res.physicalAndTraining || [];
}
// 处理收款汇总数据
const sumMonthLoading = ref<boolean | string>(loadingText);
const sumMonthData = ref<SumAndNoNeedAmortizateMonthDataItem[]>([]);
const sumMonthTotal = ref(0);
function handleSumMonthData() {
sumMonthData.value = [];
sumMonthTotal.value = 0;
try {
const paymentSummary = sumAndNoNeedAmortizateMonthData.paymentSummary;
const { sumMonthTotal: sumMonthTotalValue, resultData: sumMonthDataValue } =
handleSumAndNoNeedAmortizateMonthData(paymentSummary, {
childDictArr: [
{ title: '实物', type: 'shiwu' },
{ title: '培训班', type: 'peixun' },
{ title: '课程', type: 'kecheng' },
{ title: 'vip', type: 'vip' },
{ title: '天医币', type: 'tianyibi' },
],
typeDictArr: platList.value.map((item) => item.label),
typeKey: 'plat',
});
sumMonthData.value = sumMonthDataValue;
sumMonthTotal.value = sumMonthTotalValue;
sumMonthLoading.value = false;
} catch {
message.error('收款汇总数据解析失败');
sumMonthLoading.value = loadingError;
}
}
// 处理实物和培训班数据
const noNeedAmortizateMonthLoading = ref<boolean | string>(loadingText);
const noNeedAmortizateMonthData = ref<SumAndNoNeedAmortizateMonthDataItem[]>([]);
const noNeedAmortizateMonthTotal = ref(0);
const noNeedAmortizateList = ['实物', '培训班'];
function handleNoNeedAmortizateMonthData() {
noNeedAmortizateMonthData.value = [];
noNeedAmortizateMonthTotal.value = 0;
try {
const physicalAndTraining = sumAndNoNeedAmortizateMonthData.physicalAndTraining;
const { sumMonthTotal: sumMonthTotalValue, resultData: sumMonthDataValue } =
handleSumAndNoNeedAmortizateMonthData(physicalAndTraining, {
childDictArr: [
{ title: '微信', type: 'wx' },
{ title: '银行', type: 'bank' },
{ title: '支付宝', type: 'zfb' },
{ title: '天医币', type: 'tianyibi' },
],
typeDictArr: noNeedAmortizateList,
typeKey: 'type',
});
noNeedAmortizateMonthData.value = sumMonthDataValue;
noNeedAmortizateMonthTotal.value = sumMonthTotalValue;
noNeedAmortizateMonthLoading.value = false;
} catch {
message.error('实物和培训班数据解析失败');
noNeedAmortizateMonthLoading.value = loadingError;
}
}
async function querySumAndNoNeedAmortizateMonthData() {
sumMonthLoading.value = loadingText;
noNeedAmortizateMonthLoading.value = loadingText;
await getSumAndNoNeedAmortizateMonth();
handleSumMonthData();
handleNoNeedAmortizateMonthData();
}
// 查询和处理VIP数据
const vipMonthLoading = ref<boolean | string>(loadingText);
const vipMonthData = ref({
incomes: [] as IncomeItem[],
incomesTotal: 0,
notyet: 0,
already: 0,
now: 0,
lastNotyet: 0,
});
// 获取VIP数据
async function queryVipMonthData() {
vipMonthLoading.value = loadingText;
try {
const incomeTypes = platList.value.map((item) => item.label);
incomeTypes.push('天医币');
const res = await handleVipAndCourseMonthData(
statisticsApi.getVipStatistics,
date.value,
incomeTypes,
);
vipMonthData.value = res;
vipMonthLoading.value = false;
} catch {
message.error('VIP收入和摊销数据解析失败');
vipMonthLoading.value = loadingError;
}
}
// 查询和处理课程数据
const courseMonthLoading = ref<boolean | string>(loadingText);
const courseMonthData = ref({
incomes: [] as IncomeItem[],
incomesTotal: 0,
notyet: 0,
already: 0,
now: 0,
lastNotyet: 0,
});
// 获取课程数据
async function queryCourseMonthData() {
courseMonthLoading.value = loadingText;
try {
const incomeTypes = platList.value.map((item) => item.label);
incomeTypes.push('天医币');
const res = await handleVipAndCourseMonthData(
statisticsApi.getCourseStatistics,
date.value,
incomeTypes,
);
courseMonthData.value = res;
courseMonthLoading.value = false;
} catch {
message.error('课程收入数据解析失败');
courseMonthLoading.value = loadingError;
}
}
// 处理天医币明细数据
const tianyibiMonthLoading = ref<boolean | string>(loadingText);
const tianyibiMonthData = ref({
incomes: [] as IncomeItem[],
consumes: [] as IncomeItem[],
incomesTotal: 0,
consumesTotal: 0,
surplus: 0,
lastSurplus: 0,
});
// 获取天医币明细数据
async function queryTianyibiMonthData() {
tianyibiMonthLoading.value = loadingText;
try {
const res = await handleTianyibiDetailData(
statisticsApi.getReportTianyibi,
date.value,
platList.value.map((item) => item.label),
['培训班', '课程', 'vip', '实物'],
);
tianyibiMonthData.value = res;
tianyibiMonthLoading.value = false;
} catch {
message.error('天医币明细数据解析失败');
tianyibiMonthLoading.value = loadingError;
}
}
// 查询方法
async function query() {
loading.value = true;
await Promise.all([
querySumAndNoNeedAmortizateMonthData(),
queryVipMonthData(),
queryCourseMonthData(),
queryTianyibiMonthData(),
]);
loading.value = false;
}
// 组件挂载时查询数据
onMounted(() => {
query();
});
// 下载月总结报表
const downloadMonthStatisticsLoading = ref(false);
async function downloadMonthStatistics() {
downloadMonthStatisticsLoading.value = true;
const [year = '', month = ''] = date.value.split('-');
const Blob = await statisticsApi.downloadSumMonthStatistics({
month,
year,
});
downloadFileFromBlobPart({
source: Blob,
fileName: `${year}${month}月财务报表.xlsx`,
});
downloadMonthStatisticsLoading.value = false;
}
</script>
<template>
<Page auto-content-height>
<Loading class="flex h-full flex-col rounded-md bg-white" :spinning="loading">
<div class="search-form p-4">
<DatePicker
v-model:value="date"
picker="month"
:disabled-date="disabledDate"
value-format="YYYY-MM"
@change="query"
/>
<Button type="primary" class="ml-2" @click="query">查询</Button>
</div>
<div class="h-2 bg-gray-100"></div>
<div class="content relative min-h-2 flex-1 px-3 py-4">
<div class="max-h-full w-full overflow-auto px-1">
<Card :bordered="false" class="h-full !shadow-none">
<template #title>
<div class="flex items-center justify-between">
<div
class="absolute left-0 top-0 flex h-full items-center justify-center bg-[#1890FF] pl-5 pr-8 text-lg font-bold text-white"
style="clip-path: polygon(0 0, 100% 0, 90% 100%, 0 100%)"
>
{{ dayjs(date).format('YYYY年M月') }} 财务报表
</div>
<div class="absolute bottom-0 right-0 h-1 w-full bg-[#1890FF]"></div>
</div>
</template>
<template #extra>
<Button
:loading="downloadMonthStatisticsLoading"
type="link"
size="large"
class="!px-0"
@click="downloadMonthStatistics"
>
下载报表
</Button>
</template>
<div class="grid grid-cols-4 gap-3">
<!-- 收款汇总 -->
<div class="col-span-1 row-span-2">
<Card
class="h-full"
title="收款汇总"
size="small"
:head-style="{
backgroundColor: '#1890FF',
color: '#fff',
borderColor: '#1890FF',
}"
>
<template #extra>
<span v-if="!sumMonthLoading"> 合计{{ sumMonthTotal }} </span>
</template>
<div v-if="sumMonthLoading" class="flex h-full items-center justify-center">
{{ sumMonthLoading }}
</div>
<div v-else>
<div v-for="item in sumMonthData" :key="item.type">
<div class="font-bold">{{ `${item.type}(合计:${item.sumFee}元)` }}</div>
<div
v-for="child in item.children"
:key="child.title"
class="flex h-full items-center"
>
<div>{{ `${child.title}${child.fee}` }}</div>
</div>
</div>
</div>
</Card>
</div>
<!-- 天医币明细 -->
<div class="col-span-1 row-span-2">
<Card
class="h-full"
title="天医币明细"
size="small"
:head-style="{
backgroundColor: '#1890FF',
color: '#fff',
borderColor: '#1890FF',
}"
>
<div v-if="tianyibiMonthLoading" class="flex h-full items-center justify-center">
{{ tianyibiMonthLoading }}
</div>
<div v-else>
<div class="font-bold">
上月天医币剩余 {{ tianyibiMonthData.lastSurplus }}
</div>
<div class="font-bold">进项 合计{{ tianyibiMonthData.incomesTotal }}</div>
<div
v-for="child in tianyibiMonthData.incomes"
:key="child.type"
class="flex items-center"
>
{{
`${child.type}${child.fee}${child.point !== child.fee ? `(${child.point}天医币)` : ''}`
}}
</div>
<div class="font-bold">出项 合计{{ tianyibiMonthData.consumesTotal }}</div>
<div
v-for="child in tianyibiMonthData.consumes"
:key="child.type"
class="flex items-center"
>
{{ `${child.type}${child.fee}` }}
</div>
<div class="font-bold">APP用户天医币剩余 {{ tianyibiMonthData.surplus }}</div>
</div>
</Card>
</div>
<!-- -->
<div class="col-span-2">
<Card
class="h-full"
title="实物和培训班收入"
size="small"
:head-style="{
backgroundColor: '#1890FF',
color: '#fff',
borderColor: '#1890FF',
}"
>
<div
v-if="noNeedAmortizateMonthLoading"
class="flex h-full items-center justify-center"
>
{{ noNeedAmortizateMonthLoading }}
</div>
<div v-else>
<div v-for="item in noNeedAmortizateMonthData" :key="item.type">
<div class="font-bold">{{ `${item.type}` }}</div>
<div class="grid grid-cols-3">
<div
v-for="child in item.children"
:key="child.title"
class="flex items-center"
>
{{ `${child.title}${child.fee}` }}
</div>
<div class="flex items-center">
{{ `合计:${item.sumFee}` }}
</div>
</div>
</div>
</div>
</Card>
</div>
<!-- VIP收入及摊销 -->
<div class="col-span-1">
<Card
class="h-full"
title="VIP收入及摊销"
size="small"
:head-style="{
backgroundColor: '#1890FF',
color: '#fff',
borderColor: '#1890FF',
}"
>
<div v-if="vipMonthLoading" class="flex h-full items-center justify-center">
{{ vipMonthLoading }}
</div>
<div v-else>
<div class="font-bold">上月剩余摊销 {{ vipMonthData.lastNotyet }}</div>
<div class="font-bold">收入 合计{{ vipMonthData.incomesTotal }}</div>
<div
v-for="child in vipMonthData.incomes"
:key="child.type"
class="flex items-center"
>
{{ `${child.type}${child.fee}` }}
</div>
<div class="font-bold">月摊销 {{ vipMonthData.now }}</div>
<div class="font-bold">已摊销 {{ vipMonthData.already }}</div>
<div class="font-bold">剩余摊销 {{ vipMonthData.notyet }}</div>
</div>
</Card>
</div>
<!-- 课程收入及摊销 -->
<div class="col-span-1">
<Card
class="h-full"
title="课程收入及摊销"
size="small"
:head-style="{
backgroundColor: '#1890FF',
color: '#fff',
borderColor: '#1890FF',
}"
>
<div v-if="courseMonthLoading" class="flex h-full items-center justify-center">
{{ courseMonthLoading }}
</div>
<div v-else>
<div class="font-bold">上月剩余摊销 {{ courseMonthData.lastNotyet }}</div>
<div class="font-bold">收入 合计{{ courseMonthData.incomesTotal }}</div>
<div
v-for="child in courseMonthData.incomes"
:key="child.type"
class="flex items-center"
>
{{ `${child.type}${child.fee}` }}
</div>
<div class="font-bold">月摊销 {{ courseMonthData.now }}</div>
<div class="font-bold">已摊销 {{ courseMonthData.already }}</div>
<div class="font-bold">剩余摊销 {{ courseMonthData.notyet }}</div>
</div>
</Card>
</div>
</div>
<div class="mt-2 flex justify-end gap-10 bg-gray-50 p-2 text-black">
<span>
经合计计算后剩余天医币
{{
tianyibiMonthLoading
? '等待计算'
: `${Number.parseFloat(
(
tianyibiMonthData.lastSurplus +
tianyibiMonthData.incomesTotal -
tianyibiMonthData.consumesTotal
).toFixed(3),
)}`
}}
</span>
<span>
经合计计算后VIP剩余摊销
{{
vipMonthLoading
? '等待计算'
: `${Number.parseFloat(
(
vipMonthData.lastNotyet +
vipMonthData.incomesTotal -
vipMonthData.now
).toFixed(3),
)}`
}}
</span>
<span>
经合计计算后课程剩余摊销
{{
courseMonthLoading
? '等待计算'
: `${Number.parseFloat(
(
courseMonthData.lastNotyet +
courseMonthData.incomesTotal -
courseMonthData.now
).toFixed(3),
)}`
}}
</span>
</div>
</Card>
</div>
</div>
</Loading>
</Page>
</template>
<style scoped>
:deep(.ant-card-head) {
position: relative;
}
</style>

View File

@@ -0,0 +1,40 @@
export interface NoNeedAmortizateMonthStatistics {
wx: number;
bank: number;
zfb: number;
tianyibi: number;
sumFee: number;
type: string;
}
export interface SumAndNoNeedAmortizateMonthDataItem {
type: string;
sumFee: number;
children: Array<{
fee: number;
title: string;
}>;
}
export interface SumMonthStatistics {
peixun: number;
shiwu: number;
kecheng: number;
tianyibi: number;
plat: string;
sumFee: number;
vip: number;
}
export interface IncomeItem {
fee: number;
type: string;
point?: number;
}
export interface VipAndCourseMonthStatistics {
incomes: IncomeItem[];
notyet: number;
already: number;
now: number;
}