refactor(reports): 优化报表导出和数据处理逻辑

- 异步动态引入 XLSX 库以优化加载性能
- 抽取导出表格为工作簿的异步函数,提高代码复用性
- 使用构建函数封装报表数据行和汇总的生成逻辑
- 替换硬编码数据聚合为调用构建函数,简化代码结构
- 在导出功能中支持异步操作,增强用户体验
- 统一格式化和统计报表数据,提升代码可维护性
This commit is contained in:
2026-06-10 11:06:44 +08:00
parent dfd475856e
commit 1c80a3ae75
2 changed files with 182 additions and 81 deletions

View File

@@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import * as XLSX from "xlsx";
import {
CalendarDays,
CircleDollarSign,
@@ -279,7 +278,7 @@ function toCsvValue(value: ExportCell): string {
return stringValue;
}
function exportRows(rows: ExportRow[], filename: string, sheetName: string, format: ExportFormat): void {
async function exportRows(rows: ExportRow[], filename: string, sheetName: string, format: ExportFormat): Promise<void> {
if (rows.length === 0) {
throw new LotteryApiBizError("no_data", -1, null);
}
@@ -295,12 +294,160 @@ function exportRows(rows: ExportRow[], filename: string, sheetName: string, form
return;
}
const XLSX = await import("xlsx");
const worksheet = XLSX.utils.json_to_sheet(rows);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
XLSX.writeFile(workbook, `${filename}.xlsx`);
}
function buildDailyProfitRowsAndSummary(
items: AdminReportDailyProfitRow[],
total: number,
t: (key: string) => string,
pageScopedLabel: (statKey: string) => string,
): Pick<Extract<ReportResult, { key: "daily_profit" }>, "rows" | "summary"> {
let totalBet = 0;
let totalPayout = 0;
let totalGross = 0;
const rows = items.map((item) => {
totalBet += item.total_bet_minor;
totalPayout += item.total_payout_minor;
totalGross += item.approx_house_gross_minor;
return {
business_date: item.business_date,
total_bet_minor: item.total_bet_minor,
total_payout_minor: item.total_payout_minor,
approx_house_gross_minor: item.approx_house_gross_minor,
};
});
return {
rows,
summary: [
{ label: t("preview.stats.records"), value: String(total) },
{ label: pageScopedLabel("bet"), value: formatPlainMoney(totalBet, "NPR") },
{ label: pageScopedLabel("payout"), value: formatPlainMoney(totalPayout, "NPR") },
{
label: pageScopedLabel("houseGross"),
value: formatPlainMoney(totalGross, "NPR"),
tone: totalGross >= 0 ? "good" : "bad",
},
],
};
}
function buildPlayerTransferRowsAndSummary(
items: AdminTransferOrderItem[],
total: number,
page: number,
perPage: number,
t: (key: string) => string,
pageScopedLabel: (statKey: string) => string,
): Pick<Extract<ReportResult, { key: "player_transfer" }>, "rows" | "summary" | "meta"> {
let transferInCount = 0;
let transferOutCount = 0;
const rows = items.map((item) => {
if (item.direction === "in") {
transferInCount += 1;
} else if (item.direction === "out") {
transferOutCount += 1;
}
return {
id: item.id,
transfer_no: item.transfer_no,
player_id: item.player_id,
username: item.username,
nickname: item.nickname,
direction: item.direction,
currency_code: item.currency_code,
amount: item.amount,
status: item.status,
external_ref_no: item.external_ref_no,
fail_reason: item.fail_reason,
created_at: formatExportInstant(item.created_at),
finished_at: formatExportInstant(item.finished_at),
};
});
return {
rows,
meta: { total, page, perPage, lastPage: Math.max(1, Math.ceil(total / perPage)) },
summary: [
{ label: t("preview.stats.records"), value: String(total) },
{ label: t("preview.stats.currentPage"), value: String(items.length) },
{ label: pageScopedLabel("transferIn"), value: String(transferInCount), tone: "good" },
{ label: pageScopedLabel("transferOut"), value: String(transferOutCount), tone: "warn" },
],
};
}
function buildPlayDimensionRowsAndSummary(
items: AdminReportPlayDimensionRow[],
total: number,
t: (key: string) => string,
pageScopedLabel: (statKey: string) => string,
): Pick<Extract<ReportResult, { key: "play_dimension" }>, "rows" | "summary"> {
let totalBet = 0;
let totalPayout = 0;
const rows = items.map((item) => {
totalBet += item.total_bet_minor;
totalPayout += item.total_payout_minor;
return {
play_code: item.play_code,
dimension: item.dimension,
total_bet_minor: item.total_bet_minor,
total_payout_minor: item.total_payout_minor,
approx_house_gross_minor: item.approx_house_gross_minor,
};
});
return {
rows,
summary: [
{ label: t("preview.stats.records"), value: String(total) },
{ label: t("preview.stats.currentPage"), value: String(items.length) },
{ label: pageScopedLabel("bet"), value: formatPlainMoney(totalBet, "NPR") },
{ label: pageScopedLabel("payout"), value: formatPlainMoney(totalPayout, "NPR") },
],
};
}
function buildRebateCommissionRowsAndSummary(
items: AdminReportRebateCommissionRow[],
total: number,
t: (key: string) => string,
pageScopedLabel: (statKey: string) => string,
): Pick<Extract<ReportResult, { key: "rebate_commission" }>, "rows" | "summary"> {
let totalRebate = 0;
let totalOrders = 0;
const rows = items.map((item) => {
totalRebate += item.total_rebate_minor;
totalOrders += item.order_count;
return {
play_code: item.play_code,
total_rebate_minor: item.total_rebate_minor,
order_count: item.order_count,
ticket_item_count: item.ticket_item_count,
};
});
return {
rows,
summary: [
{ label: t("preview.stats.records"), value: String(total) },
{ label: t("preview.stats.currentPage"), value: String(items.length) },
{ label: pageScopedLabel("rebate"), value: formatPlainMoney(totalRebate, "NPR") },
{ label: pageScopedLabel("orders"), value: String(totalOrders) },
],
};
}
function metaFromList(meta: { current_page: number; per_page: number; total: number; last_page: number }): ReportMeta {
return {
total: meta.total,
@@ -724,30 +871,13 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
const payload = await getAdminReportDailyProfit(
reportListParams(filters, page, perPage),
);
const rows = payload.items.map((item) => ({
business_date: item.business_date,
total_bet_minor: item.total_bet_minor,
total_payout_minor: item.total_payout_minor,
approx_house_gross_minor: item.approx_house_gross_minor,
}));
const totalBet = payload.items.reduce((sum, item) => sum + item.total_bet_minor, 0);
const totalPayout = payload.items.reduce((sum, item) => sum + item.total_payout_minor, 0);
const totalGross = payload.items.reduce((sum, item) => sum + item.approx_house_gross_minor, 0);
const next = buildDailyProfitRowsAndSummary(payload.items, payload.meta.total, t, pageScopedLabel);
setResult({
key: "daily_profit",
raw: payload.items,
rows,
rows: next.rows,
meta: metaFromList(payload.meta),
summary: [
{ label: t("preview.stats.records"), value: String(payload.meta.total) },
{ label: pageScopedLabel("bet"), value: formatPlainMoney(totalBet, "NPR") },
{ label: pageScopedLabel("payout"), value: formatPlainMoney(totalPayout, "NPR") },
{
label: pageScopedLabel("houseGross"),
value: formatPlainMoney(totalGross, "NPR"),
tone: totalGross >= 0 ? "good" : "bad",
},
],
summary: next.summary,
});
break;
}
@@ -792,32 +922,20 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
created_from: filters.dateFrom || undefined,
created_to: filters.dateTo || undefined,
});
const rows = payload.items.map((item) => ({
id: item.id,
transfer_no: item.transfer_no,
player_id: item.player_id,
username: item.username,
nickname: item.nickname,
direction: item.direction,
currency_code: item.currency_code,
amount: item.amount,
status: item.status,
external_ref_no: item.external_ref_no,
fail_reason: item.fail_reason,
created_at: formatExportInstant(item.created_at),
finished_at: formatExportInstant(item.finished_at),
}));
const next = buildPlayerTransferRowsAndSummary(
payload.items,
payload.total,
payload.page,
payload.per_page,
t,
pageScopedLabel,
);
setResult({
key: "player_transfer",
raw: payload.items,
rows,
meta: { total: payload.total, page: payload.page, perPage: payload.per_page, lastPage: Math.max(1, Math.ceil(payload.total / payload.per_page)) },
summary: [
{ label: t("preview.stats.records"), value: String(payload.total) },
{ label: t("preview.stats.currentPage"), value: String(payload.items.length) },
{ label: pageScopedLabel("transferIn"), value: String(payload.items.filter((item) => item.direction === "in").length), tone: "good" },
{ label: pageScopedLabel("transferOut"), value: String(payload.items.filter((item) => item.direction === "out").length), tone: "warn" },
],
rows: next.rows,
meta: next.meta,
summary: next.summary,
});
break;
}
@@ -913,24 +1031,13 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
const payload = await getAdminReportPlayDimension(
reportListParams(filters, page, perPage),
);
const rows = payload.items.map((item) => ({
play_code: item.play_code,
dimension: item.dimension,
total_bet_minor: item.total_bet_minor,
total_payout_minor: item.total_payout_minor,
approx_house_gross_minor: item.approx_house_gross_minor,
}));
const next = buildPlayDimensionRowsAndSummary(payload.items, payload.meta.total, t, pageScopedLabel);
setResult({
key: "play_dimension",
raw: payload.items,
rows,
rows: next.rows,
meta: metaFromList(payload.meta),
summary: [
{ label: t("preview.stats.records"), value: String(payload.meta.total) },
{ label: t("preview.stats.currentPage"), value: String(payload.items.length) },
{ label: pageScopedLabel("bet"), value: formatPlainMoney(payload.items.reduce((s, i) => s + i.total_bet_minor, 0), "NPR") },
{ label: pageScopedLabel("payout"), value: formatPlainMoney(payload.items.reduce((s, i) => s + i.total_payout_minor, 0), "NPR") },
],
summary: next.summary,
});
break;
}
@@ -938,23 +1045,13 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
const payload = await getAdminReportRebateCommission(
reportListParams(filters, page, perPage),
);
const rows = payload.items.map((item) => ({
play_code: item.play_code,
total_rebate_minor: item.total_rebate_minor,
order_count: item.order_count,
ticket_item_count: item.ticket_item_count,
}));
const next = buildRebateCommissionRowsAndSummary(payload.items, payload.meta.total, t, pageScopedLabel);
setResult({
key: "rebate_commission",
raw: payload.items,
rows,
rows: next.rows,
meta: metaFromList(payload.meta),
summary: [
{ label: t("preview.stats.records"), value: String(payload.meta.total) },
{ label: t("preview.stats.currentPage"), value: String(payload.items.length) },
{ label: pageScopedLabel("rebate"), value: formatPlainMoney(payload.items.reduce((s, i) => s + i.total_rebate_minor, 0), "NPR") },
{ label: pageScopedLabel("orders"), value: String(payload.items.reduce((s, i) => s + i.order_count, 0)) },
],
summary: next.summary,
});
break;
}