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

@@ -2,6 +2,7 @@
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { getAdminDrawFinanceSummary } from "@/api/admin-draws";
import { postAdminRunDrawSettlement } from "@/api/admin-settlement";
@@ -21,6 +22,7 @@ import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance
import { toast } from "sonner";
export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactElement {
const { t } = useTranslation(["draws", "common"]);
const idNum = Number(drawId);
const [data, setData] = useState<AdminDrawFinanceSummaryData | null>(null);
const [err, setErr] = useState<string | null>(null);
@@ -29,7 +31,7 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
const load = useCallback(async () => {
if (!Number.isFinite(idNum) || idNum < 1) {
setErr("无效的期号 ID");
setErr(t("invalidDrawId"));
setLoading(false);
return;
}
@@ -38,22 +40,22 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
try {
setData(await getAdminDrawFinanceSummary(idNum));
} catch (e) {
setErr(e instanceof LotteryApiBizError ? e.message : "加载失败");
setErr(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
setData(null);
} finally {
setLoading(false);
}
}, [idNum]);
}, [idNum, t]);
async function runSettlement(): Promise<void> {
if (!Number.isFinite(idNum) || idNum < 1) return;
setSettling(true);
try {
const res = await postAdminRunDrawSettlement(idNum);
toast.success(res.ran ? "已触发结算" : "当前状态不可结算或已处理");
toast.success(res.ran ? t("runSettlement") : t("status"));
await load();
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : "触发结算失败");
toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed", { name: t("runSettlement") }));
} finally {
setSettling(false);
}
@@ -66,44 +68,44 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
}, [load]);
if (loading && !data) {
return <p className="text-muted-foreground text-sm"></p>;
return <p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p>;
}
if (err || !data) {
return <p className="text-destructive text-sm">{err ?? "无数据"}</p>;
return <p className="text-destructive text-sm">{err ?? t("states.noData", { ns: "common" })}</p>;
}
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardTitle className="text-lg">{t("financeOverview")}</CardTitle>
</CardHeader>
<CardContent className="grid gap-3 text-sm sm:grid-cols-2 lg:grid-cols-3">
<div>
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground">{t("drawNo")}</span>
<p className="font-mono font-semibold">{data.draw_no}</p>
</div>
<div>
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground">{t("status")}</span>
<p>{data.draw_status}</p>
</div>
<div>
<span className="text-muted-foreground"> / </span>
<span className="text-muted-foreground">{t("orderAndItemCount")}</span>
<p className="tabular-nums">
{data.order_count} / {data.ticket_item_count}
</p>
</div>
<div>
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground">{t("actualBet")}</span>
<p className="tabular-nums font-medium">{data.total_bet_minor}</p>
</div>
<div>
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground">{t("currentPayout")}</span>
<p className="tabular-nums font-medium">{data.total_payout_minor}</p>
</div>
<div>
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground">{t("grossProfit")}</span>
<p
className={cn(
"tabular-nums font-semibold",
@@ -118,38 +120,38 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
<div className="flex flex-wrap gap-2">
<Button type="button" variant="secondary" size="sm" onClick={() => void load()}>
{t("actions.refresh", { ns: "common" })}
</Button>
<Button type="button" size="sm" disabled={settling} onClick={() => void runSettlement()}>
{settling ? "处理中…" : "触发结算"}
{settling ? t("processing") : t("runSettlement")}
</Button>
<Link
href="/admin/settlement-batches"
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
>
{t("settlementBatchList")}
</Link>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
<CardTitle className="text-base">{t("relatedSettlementBatches")}</CardTitle>
</CardHeader>
<CardContent>
{data.settlement_batches.length === 0 ? (
<p className="text-muted-foreground text-sm"></p>
<p className="text-muted-foreground text-sm">{t("noSettlementBatches")}</p>
) : (
<div className="overflow-x-auto rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-20">ID</TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right">Jackpot</TableHead>
<TableHead></TableHead>
<TableHead>{t("status")}</TableHead>
<TableHead className="text-right">{t("ticketCount")}</TableHead>
<TableHead className="text-right">{t("winCount")}</TableHead>
<TableHead className="text-right">{t("payoutTotal")}</TableHead>
<TableHead className="text-right">{t("jackpot")}</TableHead>
<TableHead>{t("finishedAt")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>