feat: 增加管理端多语言与多模块界面国际化支持

This commit is contained in:
2026-05-19 09:11:55 +08:00
parent 49a4caf01e
commit 1b1dfc92ab
110 changed files with 4053 additions and 1308 deletions

View File

@@ -1,5 +1,5 @@
export const reportsModuleMeta = {
segment: "reports",
title: "报表",
title: "Reports",
description: "",
} as const;

View File

@@ -1,6 +1,7 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import {
@@ -34,20 +35,21 @@ import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminReportJobListData } from "@/types/api/admin-reports";
const REPORT_TYPES = [
{ value: "draw_profit_summary", label: "期号盈亏" },
{ value: "daily_profit_summary", label: "每日盈亏汇总" },
{ value: "player_win_loss", label: "玩家输赢报表" },
{ value: "wallet_transfer_report", label: "玩家转入转出报表" },
{ value: "hot_number_risk_report", label: "热门号码风险报表" },
{ value: "play_dimension_report", label: "玩法维度报表" },
{ value: "sold_out_number_report", label: "售罄号码报表" },
{ value: "rebate_commission_report", label: "佣金回水报表" },
{ value: "audit_operation_report", label: "后台操作审计报表" },
{ value: "wallet_txns_daily", label: "钱包流水日报" },
{ value: "transfer_orders_daily", label: "转账单日报" },
{ value: "draw_profit_summary" },
{ value: "daily_profit_summary" },
{ value: "player_win_loss" },
{ value: "wallet_transfer_report" },
{ value: "hot_number_risk_report" },
{ value: "play_dimension_report" },
{ value: "sold_out_number_report" },
{ value: "rebate_commission_report" },
{ value: "audit_operation_report" },
{ value: "wallet_txns_daily" },
{ value: "transfer_orders_daily" },
] as const;
export function ReportsConsole(): React.ReactElement {
const { t } = useTranslation(["reports", "common"]);
const formatTs = useAdminDateTimeFormatter();
const [data, setData] = useState<AdminReportJobListData | null>(null);
const [loading, setLoading] = useState(true);
@@ -67,12 +69,12 @@ export function ReportsConsole(): React.ReactElement {
const d = await getAdminReportJobs({ page, per_page: perPage });
setData(d);
} catch (e) {
setErr(e instanceof LotteryApiBizError ? e.message : "加载失败");
setErr(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
setData(null);
} finally {
setLoading(false);
}
}, [page, perPage]);
}, [page, perPage, t]);
useEffect(() => {
queueMicrotask(() => {
@@ -87,7 +89,7 @@ export function ReportsConsole(): React.ReactElement {
try {
filter_json = JSON.parse(trimmed) as Record<string, unknown>;
} catch {
toast.error("筛选 JSON 无法解析");
toast.error(t("parseFilterFailed"));
return;
}
}
@@ -99,11 +101,11 @@ export function ReportsConsole(): React.ReactElement {
parameters: filter_json,
filter_json,
});
toast.success("已创建导出任务");
toast.success(t("createSuccess"));
setPage(1);
await load();
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : "创建失败");
toast.error(e instanceof LotteryApiBizError ? e.message : t("createFailed"));
} finally {
setSubmitting(false);
}
@@ -119,7 +121,7 @@ export function ReportsConsole(): React.ReactElement {
a.click();
URL.revokeObjectURL(url);
} catch {
toast.error("下载失败");
toast.error(t("downloadFailed"));
}
}
@@ -132,11 +134,11 @@ export function ReportsConsole(): React.ReactElement {
<div className="flex w-full max-w-none flex-col gap-8">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardTitle>{t("createExport")}</CardTitle>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div className="grid gap-1.5">
<Label></Label>
<Label>{t("reportType")}</Label>
<Select
modal={false}
value={reportType}
@@ -152,14 +154,14 @@ export function ReportsConsole(): React.ReactElement {
<SelectContent>
{REPORT_TYPES.map((o) => (
<SelectItem key={o.value} value={o.value}>
{o.label}
{t(`reportTypes.${o.value}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-1.5">
<Label></Label>
<Label>{t("exportFormat")}</Label>
<Select
modal={false}
value={exportFormat}
@@ -179,7 +181,7 @@ export function ReportsConsole(): React.ReactElement {
</Select>
</div>
<div className="sm:col-span-2 lg:col-span-3 grid gap-1.5">
<Label htmlFor="report-filter-json">filter_json</Label>
<Label htmlFor="report-filter-json">{t("filterJson")}</Label>
<Textarea
id="report-filter-json"
value={filterJsonText}
@@ -190,7 +192,7 @@ export function ReportsConsole(): React.ReactElement {
</div>
<div className="sm:col-span-2 lg:col-span-3">
<Button type="button" onClick={() => void onCreate()} disabled={submitting}>
{submitting ? "提交中…" : "创建任务"}
{submitting ? t("actions.submitting", { ns: "common" }) : t("actions.createTask", { ns: "common" })}
</Button>
</div>
</CardContent>
@@ -199,16 +201,16 @@ export function ReportsConsole(): React.ReactElement {
<Card>
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-4">
<div>
<CardTitle></CardTitle>
<CardTitle>{t("taskList")}</CardTitle>
</div>
<Button type="button" variant="secondary" size="sm" onClick={() => void load()}>
{t("actions.refresh", { ns: "common" })}
</Button>
</CardHeader>
<CardContent className="space-y-4">
{err ? <p className="text-sm text-red-600 dark:text-red-400">{err}</p> : null}
{loading && !data ? (
<p className="text-muted-foreground text-sm"></p>
<p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p>
) : null}
{data ? (
<>
@@ -216,21 +218,21 @@ export function ReportsConsole(): React.ReactElement {
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-24">ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-24">{t("id")}</TableHead>
<TableHead>{t("jobId")}</TableHead>
<TableHead>{t("type")}</TableHead>
<TableHead>{t("format")}</TableHead>
<TableHead>{t("status")}</TableHead>
<TableHead>{t("output")}</TableHead>
<TableHead>{t("download")}</TableHead>
<TableHead>{t("createdAt")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.items.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-muted-foreground">
{t("empty")}
</TableCell>
</TableRow>
) : (
@@ -238,7 +240,11 @@ export function ReportsConsole(): React.ReactElement {
<TableRow key={row.id}>
<TableCell className="tabular-nums">{row.id}</TableCell>
<TableCell className="font-mono text-xs">{row.job_no}</TableCell>
<TableCell className="text-sm">{row.report_type}</TableCell>
<TableCell className="text-sm">
{t(`reportTypes.${row.report_type}`, {
defaultValue: row.report_type,
})}
</TableCell>
<TableCell>{row.export_format}</TableCell>
<TableCell>
<Badge variant="secondary">{row.status}</Badge>
@@ -253,7 +259,7 @@ export function ReportsConsole(): React.ReactElement {
size="sm"
onClick={() => void onDownload(row.id)}
>
{t("actions.download", { ns: "common" })}
</Button>
</TableCell>
<TableCell className="whitespace-nowrap font-mono text-[11px] text-muted-foreground">