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 { toast } from "sonner";
import {
@@ -37,6 +38,7 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
}
export function DrawDetailConsole({ drawId }: { drawId: string }) {
const { t } = useTranslation(["draws", "common"]);
const idNum = Number(drawId);
const profile = useAdminProfile();
const canManageDraw = adminHasAnyPermission(profile?.permissions, [PRD_DRAW_RESULT_MANAGE]);
@@ -49,7 +51,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
const load = useCallback(async () => {
if (!Number.isFinite(idNum)) {
setError("无效的期号 ID");
setError(t("invalidDrawId"));
setLoading(false);
return;
}
@@ -59,21 +61,21 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
setData(await getAdminDraw(idNum));
} catch (e) {
setData(null);
setError(e instanceof LotteryApiBizError ? e.message : "加载失败");
setError(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
} finally {
setLoading(false);
}
}, [idNum]);
}, [idNum, t]);
async function runAction(name: string, action: () => Promise<unknown>): Promise<void> {
if (!Number.isFinite(idNum)) return;
setActing(name);
try {
await action();
toast.success(`${name}成功`);
toast.success(t("actionSuccess", { name }));
await load();
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : `${name}失败`);
toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed", { name }));
} finally {
setActing(null);
}
@@ -87,11 +89,11 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
}, [load]);
if (loading && !data) {
return <p className="text-sm text-muted-foreground"></p>;
return <p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>;
}
if (error || !data) {
return <p className="text-sm text-destructive">{error ?? "无数据"}</p>;
return <p className="text-sm text-destructive">{error ?? t("states.noData", { ns: "common" })}</p>;
}
return (
@@ -101,46 +103,49 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<CardTitle className="text-xl">{data.draw_no}</CardTitle>
<p className="mt-1 text-sm text-muted-foreground"></p>
<p className="mt-1 text-sm text-muted-foreground">{t("drawDetail")}</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<DrawStatusBadge status={data.status} label={data.status} />
<DrawStatusBadge status={data.hall_preview_status} label={`大厅预览 ${data.hall_preview_status}`} />
<DrawStatusBadge
status={data.hall_preview_status}
label={t("hallPreviewStatus", { status: data.hall_preview_status })}
/>
</div>
</div>
</CardHeader>
<CardContent className="grid gap-6 p-6 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<Field label="业务日">{data.business_date}</Field>
<Field label="流水序号">{data.sequence_no}</Field>
<Field label="开始时间">{formatDt(data.start_time)}</Field>
<Field label="封盘时间">{formatDt(data.close_time)}</Field>
<Field label="计划开奖">{formatDt(data.draw_time)}</Field>
<Field label="冷静期结束">{formatDt(data.cooling_end_time)}</Field>
<Field label={t("businessDate")}>{data.business_date}</Field>
<Field label={t("sequenceNo")}>{data.sequence_no}</Field>
<Field label={t("startTime")}>{formatDt(data.start_time)}</Field>
<Field label={t("closeTime")}>{formatDt(data.close_time)}</Field>
<Field label={t("plannedDraw")}>{formatDt(data.draw_time)}</Field>
<Field label={t("coolingEndTime")}>{formatDt(data.cooling_end_time)}</Field>
</div>
<Separator />
<div className="grid gap-4 sm:grid-cols-2">
<Field label="结果来源">{data.result_source ?? "—"}</Field>
<Field label="当前结果版本">{data.current_result_version}</Field>
<Field label="结算版本">{data.settle_version}</Field>
<Field label="是否重开">{data.is_reopened ? "是" : "否"}</Field>
<Field label={t("resultSource")}>{data.result_source ?? "—"}</Field>
<Field label={t("currentResultVersion")}>{data.current_result_version}</Field>
<Field label={t("settleVersion")}>{data.settle_version}</Field>
<Field label={t("isReopened")}>{data.is_reopened ? t("yes") : t("no")}</Field>
</div>
</div>
<div className="rounded-xl border bg-muted/20 p-4">
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-sm font-medium text-muted-foreground">{t("batchStats")}</p>
<div className="mt-3 grid gap-3 text-sm">
<div className="flex items-center justify-between rounded-lg bg-background px-3 py-2">
<span></span>
<span>{t("batchTotal")}</span>
<span className="font-semibold">{data.result_batch_counts.total}</span>
</div>
<div className="flex items-center justify-between rounded-lg bg-background px-3 py-2 text-amber-600 dark:text-amber-400">
<span></span>
<span>{t("pendingReview")}</span>
<span className="font-semibold">{data.result_batch_counts.pending_review}</span>
</div>
<div className="flex items-center justify-between rounded-lg bg-background px-3 py-2 text-emerald-600 dark:text-emerald-400">
<span></span>
<span>{t("published")}</span>
<span className="font-semibold">{data.result_batch_counts.published}</span>
</div>
</div>
@@ -148,7 +153,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
href={`/admin/draws/${drawId}/finance`}
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "mt-4 w-full")}
>
{t("viewFinance")}
</Link>
</div>
</CardContent>
@@ -156,9 +161,9 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
<CardTitle className="text-base">{t("drawActions")}</CardTitle>
<p className="text-sm text-muted-foreground">
/ / RNG / /
{t("drawActionsDesc")}
</p>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
@@ -166,42 +171,42 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
type="button"
variant="secondary"
disabled={!canManageDraw || acting !== null || !["pending", "open"].includes(data.status)}
onClick={() => void runAction("手动封盘", () => postAdminManualCloseDraw(idNum))}
onClick={() => void runAction(t("manualClose"), () => postAdminManualCloseDraw(idNum))}
>
{acting === "手动封盘" ? "处理中…" : "手动封盘"}
{acting === t("manualClose") ? t("processing") : t("manualClose")}
</Button>
<Button
type="button"
variant="outline"
disabled={!canManageDraw || acting !== null || !["pending", "open", "closing", "closed"].includes(data.status)}
onClick={() => void runAction("取消期号", () => postAdminCancelDraw(idNum))}
onClick={() => void runAction(t("cancelDraw"), () => postAdminCancelDraw(idNum))}
>
{acting === "取消期号" ? "处理中…" : "未开奖前取消"}
{acting === t("cancelDraw") ? t("processing") : t("cancelBeforeDraw")}
</Button>
<Button
type="button"
disabled={!canManageDraw || acting !== null || data.status !== "closed"}
onClick={() => void runAction("RNG开奖", () => postAdminRunDrawRng(idNum))}
onClick={() => void runAction(t("rngDraw"), () => postAdminRunDrawRng(idNum))}
>
{acting === "RNG开奖" ? "生成中…" : "RNG 自动生成"}
{acting === t("rngDraw") ? t("generating") : t("rngAutoGenerate")}
</Button>
{isSuperAdmin ? (
<Button
type="button"
variant="destructive"
disabled={acting !== null || data.status !== "cooldown"}
onClick={() => void runAction("重开", () => postAdminReopenDraw(idNum))}
onClick={() => void runAction(t("reopen"), () => postAdminReopenDraw(idNum))}
>
{acting === "重开" ? "处理中…" : "冷静期重开"}
{acting === t("reopen") ? t("processing") : t("cooldownReopen")}
</Button>
) : null}
<Button
type="button"
variant="outline"
disabled={acting !== null || !["settling", "cooldown"].includes(data.status)}
onClick={() => void runAction("触发结算", () => postAdminRunDrawSettlement(idNum))}
onClick={() => void runAction(t("runSettlement"), () => postAdminRunDrawSettlement(idNum))}
>
{acting === "触发结算" ? "处理中…" : "触发结算"}
{acting === t("runSettlement") ? t("processing") : t("runSettlement")}
</Button>
</CardContent>
</Card>