feat(财务统计): 新增月度财务报表功能
添加月度财务报表页面及相关工具函数和类型定义 修改统计接口参数类型以支持字符串格式的月份 调整空状态显示样式
This commit is contained in:
@@ -9,7 +9,7 @@ export const statisticsApi = {
|
|||||||
* @param data.year 年份
|
* @param data.year 年份
|
||||||
* @returns 天医币报表数据
|
* @returns 天医币报表数据
|
||||||
*/
|
*/
|
||||||
getReportTianyibi: (data: { month?: string; year: number }) => {
|
getReportTianyibi: (data: { month: string; year: number | string }) => {
|
||||||
return requestClient.post('common/statistics/pointStatistics', data);
|
return requestClient.post('common/statistics/pointStatistics', data);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export const statisticsApi = {
|
|||||||
* @param data.year 年份
|
* @param data.year 年份
|
||||||
* @returns 实物报表数据
|
* @returns 实物报表数据
|
||||||
*/
|
*/
|
||||||
getPhysicalStatistics: (data: { month?: string; year: number }) => {
|
getPhysicalStatistics: (data: { month: string; year: number | string }) => {
|
||||||
return requestClient.post('common/statistics/physicalStatistics', data);
|
return requestClient.post('common/statistics/physicalStatistics', data);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export const statisticsApi = {
|
|||||||
* @param data.year 年份
|
* @param data.year 年份
|
||||||
* @returns 培训班报表数据
|
* @returns 培训班报表数据
|
||||||
*/
|
*/
|
||||||
getTrainingClassStatistics: (data: { month?: string; year: number }) => {
|
getTrainingClassStatistics: (data: { month: string; year: number | string }) => {
|
||||||
return requestClient.post('common/statistics/trainingClassStatistics', data);
|
return requestClient.post('common/statistics/trainingClassStatistics', data);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export const statisticsApi = {
|
|||||||
* @param data.year 年份
|
* @param data.year 年份
|
||||||
* @returns VIP报表数据
|
* @returns VIP报表数据
|
||||||
*/
|
*/
|
||||||
getVipStatistics: (data: { month?: string; year: number }) => {
|
getVipStatistics: (data: { month: string; year: number | string }) => {
|
||||||
return requestClient.post('common/statistics/vipStatistics', data);
|
return requestClient.post('common/statistics/vipStatistics', data);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export const statisticsApi = {
|
|||||||
* @param data.year 年份
|
* @param data.year 年份
|
||||||
* @returns 课程报表数据
|
* @returns 课程报表数据
|
||||||
*/
|
*/
|
||||||
getCourseStatistics: (data: { month?: string; year: number }) => {
|
getCourseStatistics: (data: { month: string; year: number | string }) => {
|
||||||
return requestClient.post('common/statistics/courseStatistics', data);
|
return requestClient.post('common/statistics/courseStatistics', data);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ export const statisticsApi = {
|
|||||||
* @param data.year 年份
|
* @param data.year 年份
|
||||||
* @returns 天医币报表数据
|
* @returns 天医币报表数据
|
||||||
*/
|
*/
|
||||||
downloadReportTianyibi: (data: { month?: string; year: number }) => {
|
downloadReportTianyibi: (data: { month: string; year: number | string }) => {
|
||||||
return defaultRequestClient.download<Blob>('common/statistics/pointInfoExport', {
|
return defaultRequestClient.download<Blob>('common/statistics/pointInfoExport', {
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -76,7 +76,7 @@ export const statisticsApi = {
|
|||||||
* @param data.year 年份
|
* @param data.year 年份
|
||||||
* @returns 实物报表数据
|
* @returns 实物报表数据
|
||||||
*/
|
*/
|
||||||
downloadReportPhysical: (data: { month?: string; year: number }) => {
|
downloadReportPhysical: (data: { month: string; year: number | string }) => {
|
||||||
return defaultRequestClient.download<Blob>('common/statistics/physicalInfoExport', {
|
return defaultRequestClient.download<Blob>('common/statistics/physicalInfoExport', {
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -88,7 +88,7 @@ export const statisticsApi = {
|
|||||||
* @param data.year 年份
|
* @param data.year 年份
|
||||||
* @returns 培训班报表数据
|
* @returns 培训班报表数据
|
||||||
*/
|
*/
|
||||||
downloadReportTrainingClass: (data: { month?: string; year: number }) => {
|
downloadReportTrainingClass: (data: { month: string; year: number | string }) => {
|
||||||
return defaultRequestClient.download<Blob>('common/statistics/trainingClassInfoExport', {
|
return defaultRequestClient.download<Blob>('common/statistics/trainingClassInfoExport', {
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -100,7 +100,7 @@ export const statisticsApi = {
|
|||||||
* @param data.year 年份
|
* @param data.year 年份
|
||||||
* @returns VIP报表数据
|
* @returns VIP报表数据
|
||||||
*/
|
*/
|
||||||
downloadReportVip: (data: { month?: string; year: number }) => {
|
downloadReportVip: (data: { month: string; year: number | string }) => {
|
||||||
return defaultRequestClient.download<Blob>('common/statistics/vipInfoExport', {
|
return defaultRequestClient.download<Blob>('common/statistics/vipInfoExport', {
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -112,9 +112,31 @@ export const statisticsApi = {
|
|||||||
* @param data.year 年份
|
* @param data.year 年份
|
||||||
* @returns 课程报表数据
|
* @returns 课程报表数据
|
||||||
*/
|
*/
|
||||||
downloadReportCourse: (data: { month?: string; year: number }) => {
|
downloadReportCourse: (data: { month: string; year: number | string }) => {
|
||||||
return defaultRequestClient.download<Blob>('common/statistics/courseInfoExport', {
|
return defaultRequestClient.download<Blob>('common/statistics/courseInfoExport', {
|
||||||
data,
|
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,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,6 +56,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/statistics/course-report',
|
path: '/statistics/course-report',
|
||||||
component: () => import('#/views/statistics/course/report.vue'),
|
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'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const handleDownloadMonth = async (index: number) => {
|
|||||||
<div class="content relative min-h-2 flex-1 px-3 py-4">
|
<div class="content relative min-h-2 flex-1 px-3 py-4">
|
||||||
<div
|
<div
|
||||||
v-if="list.length === 0 && !loading"
|
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 />
|
<Empty />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
127
apps/finance/src/views/statistics/summary-month/report-utils.ts
Normal file
127
apps/finance/src/views/statistics/summary-month/report-utils.ts
Normal 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),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
530
apps/finance/src/views/statistics/summary-month/report.vue
Normal file
530
apps/finance/src/views/statistics/summary-month/report.vue
Normal 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>
|
||||||
40
apps/finance/src/views/statistics/summary-month/types.d.ts
vendored
Normal file
40
apps/finance/src/views/statistics/summary-month/types.d.ts
vendored
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user