feat(i18n, config): 增强多语言支持并新增系统配置项

更新英文、尼泊尔语与中文语言包,新增默认币种、开奖间隔、投注窗口及相关设置的翻译文案,提升界面清晰度与用户体验。
在系统设置中新增开奖相关配置字段,增强彩票系统参数管理的灵活性。
优化多语言国际化支持,确保后台管理界面在不同语言环境下的信息表达一致性。
This commit is contained in:
2026-05-28 14:51:07 +08:00
parent b76ade9608
commit 0bfcf6c59c
7 changed files with 230 additions and 4 deletions

View File

@@ -182,6 +182,14 @@
"fields": {
"manualReview": "Require manual review for draw results",
"cooldownMinutes": "Cooldown duration (minutes)",
"defaultCurrency": "Default currency code",
"drawIntervalMinutes": "Draw interval (minutes)",
"drawBettingWindowSeconds": "Betting window (seconds)",
"drawCloseBeforeDrawSeconds": "Close before draw (seconds)",
"drawBufferDrawsAhead": "Pre-generated future draws",
"currencyDisplayDecimals": "Display decimals",
"currencyDecimalSeparator": "Decimal separator",
"currencyThousandsSeparator": "Thousands separator",
"autoSettlement": "Run settlement automatically",
"autoApprove": "Auto-approve settlement batches",
"autoPayout": "Auto-credit winnings to wallets",

View File

@@ -182,6 +182,14 @@
"fields": {
"manualReview": "ड्रअ परिणामका लागि म्यानुअल समीक्षा चाहिने",
"cooldownMinutes": "कूलडाउन अवधि (मिनेट)",
"defaultCurrency": "पूर्वनिर्धारित मुद्रा कोड",
"drawIntervalMinutes": "ड्रअ अन्तराल (मिनेट)",
"drawBettingWindowSeconds": "बेटिङ विन्डो (सेकेन्ड)",
"drawCloseBeforeDrawSeconds": "ड्रअ अघि बन्द (सेकेन्ड)",
"drawBufferDrawsAhead": "अग्रिम सिर्जना गरिने ड्रअ संख्या",
"currencyDisplayDecimals": "प्रदर्शन दशमलव स्थान",
"currencyDecimalSeparator": "दशमलव विभाजक",
"currencyThousandsSeparator": "हजार विभाजक",
"autoSettlement": "सेटलमेन्ट स्वतः चलाउने",
"autoApprove": "सेटलमेन्ट ब्याच स्वतः स्वीकृत",
"autoPayout": "जित रकम स्वतः वालेटमा जम्मा",

View File

@@ -182,6 +182,14 @@
"fields": {
"manualReview": "开奖结果必须人工审核",
"cooldownMinutes": "冷静期时长(分钟)",
"defaultCurrency": "默认币种代码",
"drawIntervalMinutes": "开奖间隔(分钟)",
"drawBettingWindowSeconds": "下注窗口(秒)",
"drawCloseBeforeDrawSeconds": "封盘提前(秒)",
"drawBufferDrawsAhead": "预生成未来期数",
"currencyDisplayDecimals": "金额显示小数位",
"currencyDecimalSeparator": "小数分隔符",
"currencyThousandsSeparator": "千位分隔符",
"autoSettlement": "自动执行结算",
"autoApprove": "自动审核结算批次",
"autoPayout": "自动派彩入账",

View File

@@ -14,8 +14,7 @@ export function hasLotteryAdminApiBaseUrl(): boolean {
}
export const adminHttp = axios.create({
// 统一走 Next 同源 /api 代理,由 next.config.ts 的 API_BASE_URL 转发到后端
baseURL: "/api",
// API 路径统一由调用方传 `/api/v1/...`,避免与前缀重复拼接成 `/api/api/v1/...`
timeout: 30_000,
headers: { Accept: "application/json" },
});

View File

@@ -310,7 +310,16 @@ export function PlayersConsole(): React.ReactElement {
onValueChange={(v) => setSiteCode(v === "__all__" ? "" : v ?? "")}
>
<SelectTrigger className="w-full sm:w-[12rem]">
<SelectValue placeholder={t("filterAllSites")} />
<SelectValue>
{(v) => {
const value = String(v ?? "__all__");
if (value === "__all__") {
return t("filterAllSites");
}
const site = siteOptions.find((item) => item.code === value);
return site ? `${site.code}${site.name}` : value;
}}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">{t("filterAllSites")}</SelectItem>

View File

@@ -24,8 +24,16 @@ const DRAW_GROUP = "draw";
const SETTLEMENT_GROUP = "settlement";
const DRAW_KEYS = {
DEFAULT_CURRENCY: "currency.default_code",
DRAW_INTERVAL_MINUTES: "draw.interval_minutes",
DRAW_BETTING_WINDOW_SECONDS: "draw.betting_window_seconds",
DRAW_CLOSE_BEFORE_DRAW_SECONDS: "draw.close_before_draw_seconds",
DRAW_BUFFER_DRAWS_AHEAD: "draw.buffer_draws_ahead",
REQUIRE_MANUAL_REVIEW: "draw.require_manual_review",
COOLDOWN_MINUTES: "draw.cooldown_minutes",
CURRENCY_DISPLAY_DECIMALS: "currency.display_decimals",
CURRENCY_DECIMAL_SEPARATOR: "currency.decimal_separator",
CURRENCY_THOUSANDS_SEPARATOR: "currency.thousands_separator",
AUTO_SETTLEMENT: "settlement.auto_run_on_tick",
AUTO_APPROVE: "settlement.auto_approve_on_tick",
AUTO_PAYOUT: "settlement.auto_payout_on_tick",
@@ -41,8 +49,16 @@ const FRONTEND_KEYS = {
} as const;
interface RuntimeDraft {
defaultCurrency: string;
drawIntervalMinutes: string;
drawBettingWindowSeconds: string;
drawCloseBeforeDrawSeconds: string;
drawBufferDrawsAhead: string;
requireManualReview: boolean;
cooldownMinutes: string;
currencyDisplayDecimals: string;
currencyDecimalSeparator: string;
currencyThousandsSeparator: string;
autoSettlement: boolean;
autoApprove: boolean;
autoPayout: boolean;
@@ -89,8 +105,16 @@ export function SystemSettingsScreen() {
const { t } = useTranslation(["common", "config", "adminUsers"]);
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const [draft, setDraft] = useState<RuntimeDraft>({
defaultCurrency: "NPR",
drawIntervalMinutes: "5",
drawBettingWindowSeconds: "270",
drawCloseBeforeDrawSeconds: "30",
drawBufferDrawsAhead: "8",
requireManualReview: false,
cooldownMinutes: "15",
currencyDisplayDecimals: "2",
currencyDecimalSeparator: ".",
currencyThousandsSeparator: ",",
autoSettlement: true,
autoApprove: true,
autoPayout: true,
@@ -100,8 +124,16 @@ export function SystemSettingsScreen() {
playRulesHtmlNe: "",
});
const [saved, setSaved] = useState<RuntimeDraft>({
defaultCurrency: "NPR",
drawIntervalMinutes: "5",
drawBettingWindowSeconds: "270",
drawCloseBeforeDrawSeconds: "30",
drawBufferDrawsAhead: "8",
requireManualReview: false,
cooldownMinutes: "15",
currencyDisplayDecimals: "2",
currencyDecimalSeparator: ".",
currencyThousandsSeparator: ",",
autoSettlement: true,
autoApprove: true,
autoPayout: true,
@@ -130,8 +162,16 @@ export function SystemSettingsScreen() {
const legacyHtml = String(kv[FRONTEND_KEYS.PLAY_RULES_HTML] ?? "");
const nextDraft: RuntimeDraft = {
defaultCurrency: String(kv[DRAW_KEYS.DEFAULT_CURRENCY] ?? "NPR"),
drawIntervalMinutes: String(kv[DRAW_KEYS.DRAW_INTERVAL_MINUTES] ?? 5),
drawBettingWindowSeconds: String(kv[DRAW_KEYS.DRAW_BETTING_WINDOW_SECONDS] ?? 270),
drawCloseBeforeDrawSeconds: String(kv[DRAW_KEYS.DRAW_CLOSE_BEFORE_DRAW_SECONDS] ?? 30),
drawBufferDrawsAhead: String(kv[DRAW_KEYS.DRAW_BUFFER_DRAWS_AHEAD] ?? 8),
requireManualReview: Boolean(kv[DRAW_KEYS.REQUIRE_MANUAL_REVIEW] ?? false),
cooldownMinutes: String(kv[DRAW_KEYS.COOLDOWN_MINUTES] ?? 15),
currencyDisplayDecimals: String(kv[DRAW_KEYS.CURRENCY_DISPLAY_DECIMALS] ?? 2),
currencyDecimalSeparator: String(kv[DRAW_KEYS.CURRENCY_DECIMAL_SEPARATOR] ?? "."),
currencyThousandsSeparator: String(kv[DRAW_KEYS.CURRENCY_THOUSANDS_SEPARATOR] ?? ","),
autoSettlement: Boolean(kv[DRAW_KEYS.AUTO_SETTLEMENT] ?? true),
autoApprove: Boolean(kv[DRAW_KEYS.AUTO_APPROVE] ?? true),
autoPayout: Boolean(kv[DRAW_KEYS.AUTO_PAYOUT] ?? true),
@@ -164,11 +204,43 @@ export function SystemSettingsScreen() {
const handleSave = async () => {
setSaving(true);
try {
await updateAdminSetting(
DRAW_KEYS.DEFAULT_CURRENCY,
draft.defaultCurrency.trim().toUpperCase() || "NPR",
);
await updateAdminSetting(
DRAW_KEYS.DRAW_INTERVAL_MINUTES,
Math.max(1, Number.parseInt(draft.drawIntervalMinutes || "5", 10) || 5),
);
await updateAdminSetting(
DRAW_KEYS.DRAW_BETTING_WINDOW_SECONDS,
Math.max(10, Number.parseInt(draft.drawBettingWindowSeconds || "270", 10) || 270),
);
await updateAdminSetting(
DRAW_KEYS.DRAW_CLOSE_BEFORE_DRAW_SECONDS,
Math.max(5, Number.parseInt(draft.drawCloseBeforeDrawSeconds || "30", 10) || 30),
);
await updateAdminSetting(
DRAW_KEYS.DRAW_BUFFER_DRAWS_AHEAD,
Math.max(1, Number.parseInt(draft.drawBufferDrawsAhead || "8", 10) || 8),
);
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.CURRENCY_DISPLAY_DECIMALS,
Math.max(0, Math.min(12, Number.parseInt(draft.currencyDisplayDecimals || "2", 10) || 2)),
);
await updateAdminSetting(
DRAW_KEYS.CURRENCY_DECIMAL_SEPARATOR,
(draft.currencyDecimalSeparator || ".").slice(0, 1),
);
await updateAdminSetting(
DRAW_KEYS.CURRENCY_THOUSANDS_SEPARATOR,
(draft.currencyThousandsSeparator || ",").slice(0, 1),
);
await updateAdminSetting(DRAW_KEYS.AUTO_SETTLEMENT, draft.autoSettlement);
await updateAdminSetting(DRAW_KEYS.AUTO_APPROVE, draft.autoApprove);
await updateAdminSetting(DRAW_KEYS.AUTO_PAYOUT, draft.autoPayout);
@@ -212,6 +284,119 @@ export function SystemSettingsScreen() {
<div className="h-px bg-border/60" />
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="default-currency" className="text-sm font-medium">
{t("system.fields.defaultCurrency", { ns: "config" })}
</Label>
<Input
id="default-currency"
value={draft.defaultCurrency}
onChange={(e) => updateDraft("defaultCurrency", e.target.value.toUpperCase())}
disabled={loading || saving}
maxLength={16}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="draw-interval-minutes" className="text-sm font-medium">
{t("system.fields.drawIntervalMinutes", { ns: "config" })}
</Label>
<Input
id="draw-interval-minutes"
type="number"
min="1"
max="1440"
step="1"
value={draft.drawIntervalMinutes}
onChange={(e) => updateDraft("drawIntervalMinutes", e.target.value)}
disabled={loading || saving}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="draw-betting-window-seconds" className="text-sm font-medium">
{t("system.fields.drawBettingWindowSeconds", { ns: "config" })}
</Label>
<Input
id="draw-betting-window-seconds"
type="number"
min="10"
step="1"
value={draft.drawBettingWindowSeconds}
onChange={(e) => updateDraft("drawBettingWindowSeconds", e.target.value)}
disabled={loading || saving}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="draw-close-before-seconds" className="text-sm font-medium">
{t("system.fields.drawCloseBeforeDrawSeconds", { ns: "config" })}
</Label>
<Input
id="draw-close-before-seconds"
type="number"
min="5"
step="1"
value={draft.drawCloseBeforeDrawSeconds}
onChange={(e) => updateDraft("drawCloseBeforeDrawSeconds", e.target.value)}
disabled={loading || saving}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="draw-buffer-ahead" className="text-sm font-medium">
{t("system.fields.drawBufferDrawsAhead", { ns: "config" })}
</Label>
<Input
id="draw-buffer-ahead"
type="number"
min="1"
step="1"
value={draft.drawBufferDrawsAhead}
onChange={(e) => updateDraft("drawBufferDrawsAhead", e.target.value)}
disabled={loading || saving}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="currency-display-decimals" className="text-sm font-medium">
{t("system.fields.currencyDisplayDecimals", { ns: "config" })}
</Label>
<Input
id="currency-display-decimals"
type="number"
min="0"
max="12"
step="1"
value={draft.currencyDisplayDecimals}
onChange={(e) => updateDraft("currencyDisplayDecimals", e.target.value)}
disabled={loading || saving}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="currency-decimal-separator" className="text-sm font-medium">
{t("system.fields.currencyDecimalSeparator", { ns: "config" })}
</Label>
<Input
id="currency-decimal-separator"
value={draft.currencyDecimalSeparator}
onChange={(e) => updateDraft("currencyDecimalSeparator", e.target.value)}
disabled={loading || saving}
maxLength={1}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="currency-thousands-separator" className="text-sm font-medium">
{t("system.fields.currencyThousandsSeparator", { ns: "config" })}
</Label>
<Input
id="currency-thousands-separator"
value={draft.currencyThousandsSeparator}
onChange={(e) => updateDraft("currencyThousandsSeparator", e.target.value)}
disabled={loading || saving}
maxLength={1}
/>
</div>
</div>
<div className="h-px bg-border/60" />
<div className="flex flex-wrap items-center justify-between gap-3">
<Label className="text-sm font-medium">{t("system.fields.autoSettlement", { ns: "config" })}</Label>
<Switch

View File

@@ -199,7 +199,16 @@ export function PlayerTicketsConsole(): React.ReactElement {
}
>
<SelectTrigger className="w-full sm:w-[12rem]">
<SelectValue placeholder={t("filterAllSites")} />
<SelectValue>
{(v) => {
const value = String(v ?? "__all__");
if (value === "__all__") {
return t("filterAllSites");
}
const site = siteOptions.find((item) => item.code === value);
return site ? `${site.code}${site.name}` : value;
}}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">{t("filterAllSites")}</SelectItem>