feat: 统一管理端多语言、配置与票据/结算页面重构

This commit is contained in:
2026-05-20 16:27:06 +08:00
parent 37b13278ef
commit 08a11a1589
81 changed files with 2059 additions and 490 deletions

View File

@@ -0,0 +1,226 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import {
getAdminSettings,
updateAdminSetting,
} from "@/api/admin-settings";
import { WalletConfigDocScreen } from "@/modules/config/doc/wallet-config-doc-screen";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { LotteryApiBizError } from "@/types/api/errors";
const DRAW_GROUP = "draw";
const SETTLEMENT_GROUP = "settlement";
const DRAW_KEYS = {
REQUIRE_MANUAL_REVIEW: "draw.require_manual_review",
COOLDOWN_MINUTES: "draw.cooldown_minutes",
AUTO_SETTLEMENT: "settlement.auto_run_on_tick",
} as const;
interface RuntimeDraft {
requireManualReview: boolean;
cooldownMinutes: string;
autoSettlement: boolean;
}
export function SystemSettingsScreen() {
const { t } = useTranslation(["common", "config", "adminUsers"]);
const [draft, setDraft] = useState<RuntimeDraft>({
requireManualReview: false,
cooldownMinutes: "15",
autoSettlement: true,
});
const [saved, setSaved] = useState<RuntimeDraft>({
requireManualReview: false,
cooldownMinutes: "15",
autoSettlement: true,
});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [dirty, setDirty] = useState(false);
const load = useCallback(async () => {
setLoading(true);
try {
const [drawRes, settlementRes] = await Promise.all([
getAdminSettings(DRAW_GROUP),
getAdminSettings(SETTLEMENT_GROUP),
]);
const kv: Record<string, unknown> = {};
for (const item of [...drawRes.items, ...settlementRes.items]) {
kv[item.key] = item.value;
}
const nextDraft: RuntimeDraft = {
requireManualReview: Boolean(kv[DRAW_KEYS.REQUIRE_MANUAL_REVIEW] ?? false),
cooldownMinutes: String(kv[DRAW_KEYS.COOLDOWN_MINUTES] ?? 15),
autoSettlement: Boolean(kv[DRAW_KEYS.AUTO_SETTLEMENT] ?? true),
};
setDraft(nextDraft);
setSaved(nextDraft);
setDirty(false);
} catch {
toast.error(t("system.loadFailed", { ns: "config" }));
} finally {
setLoading(false);
}
}, [t]);
useEffect(() => {
queueMicrotask(() => {
void load();
});
}, [load]);
const updateDraft = <K extends keyof RuntimeDraft>(field: K, value: RuntimeDraft[K]) => {
setDraft((prev) => ({ ...prev, [field]: value }));
setDirty(true);
};
const handleSave = async () => {
setSaving(true);
try {
await updateAdminSetting(DRAW_KEYS.REQUIRE_MANUAL_REVIEW, draft.requireManualReview);
await updateAdminSetting(
DRAW_KEYS.COOLDOWN_MINUTES,
Math.max(0, Number.parseInt(draft.cooldownMinutes || "0", 10) || 0),
);
await updateAdminSetting(DRAW_KEYS.AUTO_SETTLEMENT, draft.autoSettlement);
toast.success(t("system.saveSuccess", { ns: "config" }));
setSaved(draft);
setDirty(false);
} catch (error) {
toast.error(
error instanceof LotteryApiBizError ? error.message : t("system.saveFailed", { ns: "config" }),
);
} finally {
setSaving(false);
}
};
return (
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6">
<Card>
<CardHeader className="space-y-3">
<div className="space-y-1">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
{t("nav.settings", { ns: "common", defaultValue: "System Settings" })}
</p>
<CardTitle className="text-2xl">
{t("system.runtimeTitle", { ns: "config" })}
</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-3 text-sm text-muted-foreground">
<p>
{t("system.runtimeIntro1", { ns: "config" })}
</p>
<p>
{t("system.runtimeIntro2", { ns: "config" })}
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{t("system.title", { ns: "config" })}</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<p className="text-sm text-muted-foreground">
{t("system.description", { ns: "config" })}
</p>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-3 rounded-xl border border-border/70 p-4">
<div className="space-y-1">
<Label htmlFor="manual-review">{t("system.fields.manualReview", { ns: "config" })}</Label>
<p className="text-xs text-muted-foreground">
{t("system.hints.manualReview", { ns: "config" })}
</p>
</div>
<div className="flex items-center gap-3">
<Checkbox
id="manual-review"
checked={draft.requireManualReview}
onCheckedChange={(checked) => updateDraft("requireManualReview", checked === true)}
disabled={loading || saving}
/>
<Label htmlFor="manual-review" className="text-sm font-medium">
{draft.requireManualReview
? t("system.states.enabled", { ns: "config" })
: t("system.states.disabled", { ns: "config" })}
</Label>
</div>
</div>
<div className="space-y-3 rounded-xl border border-border/70 p-4">
<div className="space-y-1">
<Label htmlFor="auto-settlement">{t("system.fields.autoSettlement", { ns: "config" })}</Label>
<p className="text-xs text-muted-foreground">
{t("system.hints.autoSettlement", { ns: "config" })}
</p>
</div>
<div className="flex items-center gap-3">
<Checkbox
id="auto-settlement"
checked={draft.autoSettlement}
onCheckedChange={(checked) => updateDraft("autoSettlement", checked === true)}
disabled={loading || saving}
/>
<Label htmlFor="auto-settlement" className="text-sm font-medium">
{draft.autoSettlement
? t("system.states.enabled", { ns: "config" })
: t("system.states.disabled", { ns: "config" })}
</Label>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="cooldown-minutes">{t("system.fields.cooldownMinutes", { ns: "config" })}</Label>
<Input
id="cooldown-minutes"
type="number"
min="0"
step="1"
value={draft.cooldownMinutes}
onChange={(e) => updateDraft("cooldownMinutes", e.target.value)}
disabled={loading || saving}
/>
<p className="text-xs text-muted-foreground">
{t("system.hints.cooldownMinutes", { ns: "config" })}
</p>
</div>
<div className="flex items-center gap-4">
<Button onClick={() => void handleSave()} disabled={!dirty || loading || saving}>
{saving ? t("saving", { ns: "adminUsers" }) : t("actions.save", { ns: "adminUsers" })}
</Button>
{dirty && (
<Button
variant="outline"
onClick={() => {
setDraft(saved);
setDirty(false);
}}
>
{t("system.discard", { ns: "config" })}
</Button>
)}
</div>
</CardContent>
</Card>
<WalletConfigDocScreen />
</div>
);
}