feat: 统一管理端多语言、配置与票据/结算页面重构
This commit is contained in:
226
src/modules/settings/system-settings-screen.tsx
Normal file
226
src/modules/settings/system-settings-screen.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user