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,4 +1,4 @@
export const settlementModuleMeta = {
title: "结算",
title: "Settlement",
description: "",
} as const;

View File

@@ -2,6 +2,7 @@
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import {
@@ -38,6 +39,7 @@ type Props = {
};
export function SettlementBatchDetailsConsole({ batchId }: Props) {
const { t } = useTranslation(["settlement", "common"]);
const formatDt = useAdminDateTimeFormatter();
const [summary, setSummary] = useState<AdminSettlementBatchShowData | null>(null);
const [details, setDetails] = useState<AdminSettlementBatchDetailsData | null>(null);
@@ -58,29 +60,29 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
setSummary(s);
setDetails(d);
} catch (e) {
setErr(e instanceof LotteryApiBizError ? e.message : "加载失败");
setErr(e instanceof LotteryApiBizError ? e.message : t("loadFailed"));
setSummary(null);
setDetails(null);
} finally {
setLoading(false);
}
}, [batchId, page, perPage]);
}, [batchId, page, perPage, t]);
async function runAction(label: string, action: () => Promise<unknown>): Promise<void> {
setActing(label);
try {
await action();
toast.success(`${label}成功`);
toast.success(t("actionSuccess", { name: label }));
await load();
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : `${label}失败`);
toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed", { name: label }));
} finally {
setActing(null);
}
}
async function exportCsv(): Promise<void> {
setActing("导出");
setActing(t("export"));
try {
const blob = await downloadAdminSettlementBatchExport(batchId);
const url = URL.createObjectURL(blob);
@@ -92,7 +94,7 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
a.remove();
URL.revokeObjectURL(url);
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : "导出失败");
toast.error(e instanceof LotteryApiBizError ? e.message : t("exportFailed"));
} finally {
setActing(null);
}
@@ -107,19 +109,19 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
<ModuleScaffold>
<div className="mb-4">
<Link href="/admin/settlement-batches" className={cn(buttonVariants({ variant: "ghost", size: "sm" }), "px-0")}>
{t("backToList")}
</Link>
</div>
{err ? (
<Card className="border-destructive/40">
<CardHeader>
<CardTitle className="text-base"></CardTitle>
<CardTitle className="text-base">{t("errorTitle")}</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<p className="text-sm text-destructive">{err}</p>
<Button type="button" size="sm" variant="secondary" onClick={() => void load()}>
{t("retry")}
</Button>
</CardContent>
</Card>
@@ -128,44 +130,47 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
{summary ? (
<Card className="mb-6">
<CardHeader>
<CardTitle className="font-mono text-base"> #{summary.id}</CardTitle>
<CardTitle className="font-mono text-base">{t("batchSummary", { id: summary.id })}</CardTitle>
<p className="text-sm text-muted-foreground">
{summary.draw_no ?? "—"} · {summary.draw_status ?? "—"} · v
{summary.result_batch_version ?? "—"}
{t("summaryMeta", {
drawNo: summary.draw_no ?? "—",
drawStatus: summary.draw_status ?? "—",
version: summary.result_batch_version ?? "—",
})}
</p>
</CardHeader>
<CardContent className="grid gap-2 text-sm sm:grid-cols-2">
<p>
<span className="text-muted-foreground"></span>{" "}
<span className="text-muted-foreground">{t("settlementStatus")}</span>{" "}
<span className="font-mono">{summary.status}</span>
</p>
<p>
<span className="text-muted-foreground"></span>{" "}
<span className="text-muted-foreground">{t("reviewState")}</span>{" "}
<span className="font-mono">{summary.review_status ?? "—"}</span>
</p>
<p>
<span className="text-muted-foreground"></span>{" "}
<span className="text-muted-foreground">{t("ticketTotal")}</span>{" "}
<span className="tabular-nums">{summary.total_ticket_count}</span>
</p>
<p>
<span className="text-muted-foreground"></span>{" "}
<span className="text-muted-foreground">{t("winTotal")}</span>{" "}
<span className="tabular-nums">{summary.total_win_count}</span>
</p>
<p>
<span className="text-muted-foreground"></span>{" "}
<span className="text-muted-foreground">{t("payoutAmount")}</span>{" "}
<span className="font-mono tabular-nums">{formatAdminMinorUnits(summary.total_payout_amount)}</span>
</p>
<p>
<span className="text-muted-foreground">Jackpot </span>{" "}
<span className="text-muted-foreground">{t("jackpotPayout")}</span>{" "}
<span className="font-mono tabular-nums">
{formatAdminMinorUnits(summary.total_jackpot_payout_amount)}
</span>
</p>
<p>
<span className="text-muted-foreground"></span> {formatDt(summary.started_at)}
<span className="text-muted-foreground">{t("startedAt")}</span> {formatDt(summary.started_at)}
</p>
<p>
<span className="text-muted-foreground"></span> {formatDt(summary.finished_at)}
<span className="text-muted-foreground">{t("endedAt")}</span> {formatDt(summary.finished_at)}
</p>
<div className="flex flex-wrap gap-2 sm:col-span-2">
<Button
@@ -173,40 +178,40 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
size="sm"
variant="outline"
disabled={acting !== null || summary.status !== "pending_review"}
onClick={() => void runAction("审核通过", () => postAdminApproveSettlementBatch(batchId))}
onClick={() => void runAction(t("approve"), () => postAdminApproveSettlementBatch(batchId))}
>
{t("approve")}
</Button>
<Button
type="button"
size="sm"
variant="outline"
disabled={acting !== null || summary.status !== "pending_review"}
onClick={() => void runAction("驳回", () => postAdminRejectSettlementBatch(batchId))}
onClick={() => void runAction(t("reject"), () => postAdminRejectSettlementBatch(batchId))}
>
{t("reject")}
</Button>
<Button
type="button"
size="sm"
disabled={acting !== null || summary.status !== "approved"}
onClick={() => void runAction("执行派彩", () => postAdminPayoutSettlementBatch(batchId))}
onClick={() => void runAction(t("runPayout"), () => postAdminPayoutSettlementBatch(batchId))}
>
{t("runPayout")}
</Button>
<Button type="button" size="sm" variant="secondary" disabled={acting !== null} onClick={() => void exportCsv()}>
{t("exportSettlementReport")}
</Button>
</div>
</CardContent>
</Card>
) : loading ? (
<p className="text-muted-foreground text-sm"></p>
<p className="text-muted-foreground text-sm">{t("loadingSummary")}</p>
) : null}
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
<CardTitle className="text-base">{t("detailTitle")}</CardTitle>
</CardHeader>
<CardContent>
{details ? (
@@ -214,12 +219,12 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right">Jackpot</TableHead>
<TableHead>{t("ticketNo")}</TableHead>
<TableHead>{t("playCode")}</TableHead>
<TableHead>{t("player")}</TableHead>
<TableHead>{t("matchedTier")}</TableHead>
<TableHead className="text-right">{t("regularPayout")}</TableHead>
<TableHead className="text-right">{t("jackpot")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -256,7 +261,9 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
/>
</>
) : (
<p className="text-muted-foreground text-sm">{loading ? "加载明细…" : "无数据"}</p>
<p className="text-muted-foreground text-sm">
{loading ? t("loadingDetails") : t("states.noData", { ns: "common" })}
</p>
)}
</CardContent>
</Card>

View File

@@ -2,6 +2,7 @@
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import {
@@ -42,13 +43,14 @@ import { settlementModuleMeta } from "@/modules/settlement/meta";
const STATUS_ALL = "__all__";
const STATUS_OPTIONS: { value: string; label: string }[] = [
{ value: STATUS_ALL, label: "不限" },
{ value: "running", label: "进行中" },
{ value: "completed", label: "已完成" },
{ value: "failed", label: "失败" },
{ value: STATUS_ALL, label: "statusOptions.all" },
{ value: "running", label: "statusOptions.running" },
{ value: "completed", label: "statusOptions.completed" },
{ value: "failed", label: "statusOptions.failed" },
];
export function SettlementBatchesConsole() {
const { t } = useTranslation(["settlement", "common"]);
const formatDt = useAdminDateTimeFormatter();
const [data, setData] = useState<AdminSettlementBatchListData | null>(null);
const [loading, setLoading] = useState(true);
@@ -76,12 +78,12 @@ export function SettlementBatchesConsole() {
});
setData(d);
} catch (e) {
setError(e instanceof LotteryApiBizError ? e.message : "加载失败");
setError(e instanceof LotteryApiBizError ? e.message : t("loadFailed"));
setData(null);
} finally {
setLoading(false);
}
}, [page, perPage, appliedDrawNo, appliedStatus]);
}, [page, perPage, appliedDrawNo, appliedStatus, t]);
useEffect(() => {
const t = window.setTimeout(() => void load(), 0);
@@ -98,10 +100,10 @@ export function SettlementBatchesConsole() {
setActingId(batchId);
try {
await action();
toast.success(`${label}成功`);
toast.success(t("actionSuccess", { name: label }));
await load();
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : `${label}失败`);
toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed", { name: label }));
} finally {
setActingId(null);
}
@@ -120,7 +122,7 @@ export function SettlementBatchesConsole() {
a.remove();
URL.revokeObjectURL(url);
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : "导出失败");
toast.error(e instanceof LotteryApiBizError ? e.message : t("exportFailed"));
} finally {
setActingId(null);
}
@@ -133,21 +135,21 @@ export function SettlementBatchesConsole() {
</div>
<Card className="mb-6">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
<CardTitle className="text-base">{t("filter")}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-end">
<div className="flex min-w-[12rem] flex-1 flex-col gap-1.5">
<Label htmlFor="sb-draw-no"></Label>
<Label htmlFor="sb-draw-no">{t("drawNo")}</Label>
<Input
id="sb-draw-no"
value={draftDrawNo}
onChange={(e) => setDraftDrawNo(e.target.value)}
placeholder="如 20260511-001"
placeholder={t("placeholderDrawNo")}
className="font-mono"
/>
</div>
<div className="flex min-w-[10rem] flex-col gap-1.5">
<Label></Label>
<Label>{t("status")}</Label>
<Select value={draftStatus} onValueChange={(v) => setDraftStatus(v ?? STATUS_ALL)}>
<SelectTrigger>
<SelectValue />
@@ -155,40 +157,40 @@ export function SettlementBatchesConsole() {
<SelectContent>
{STATUS_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>
{o.label}
{t(o.label)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button type="button" onClick={applyFilters}>
{t("apply")}
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
<CardTitle className="text-base">{t("batchList")}</CardTitle>
</CardHeader>
<CardContent>
{error ? <p className="text-destructive text-sm">{error}</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>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></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("drawNo")}</TableHead>
<TableHead>{t("version", { ns: "draws", version: "" }).replace(" v", "").trim()}</TableHead>
<TableHead>{t("reviewStatus")}</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>
<TableHead />
</TableRow>
</TableHeader>
@@ -230,33 +232,33 @@ export function SettlementBatchesConsole() {
href={`/admin/settlement-batches/${row.id}/details`}
className={cn(buttonVariants({ variant: "link", size: "sm" }), "px-0")}
>
{t("details")}
</Link>
<Button
type="button"
size="sm"
variant="outline"
disabled={actingId !== null || row.status !== "pending_review"}
onClick={() => void runBatchAction(row.id, "审核通过", () => postAdminApproveSettlementBatch(row.id))}
onClick={() => void runBatchAction(row.id, t("approve"), () => postAdminApproveSettlementBatch(row.id))}
>
{t("pass")}
</Button>
<Button
type="button"
size="sm"
variant="outline"
disabled={actingId !== null || row.status !== "pending_review"}
onClick={() => void runBatchAction(row.id, "驳回", () => postAdminRejectSettlementBatch(row.id))}
onClick={() => void runBatchAction(row.id, t("reject"), () => postAdminRejectSettlementBatch(row.id))}
>
{t("reject")}
</Button>
<Button
type="button"
size="sm"
disabled={actingId !== null || row.status !== "approved"}
onClick={() => void runBatchAction(row.id, "执行派彩", () => postAdminPayoutSettlementBatch(row.id))}
onClick={() => void runBatchAction(row.id, t("runPayout"), () => postAdminPayoutSettlementBatch(row.id))}
>
{t("payout")}
</Button>
<Button
type="button"
@@ -265,7 +267,7 @@ export function SettlementBatchesConsole() {
disabled={actingId !== null}
onClick={() => void exportBatch(row.id)}
>
{t("export")}
</Button>
</div>
</TableCell>