feat(i18n, config): 增强多语言支持并新增系统配置项
更新英文、尼泊尔语与中文语言包,新增默认币种、开奖间隔、投注窗口及相关设置的翻译文案,提升界面清晰度与用户体验。 在系统设置中新增开奖相关配置字段,增强彩票系统参数管理的灵活性。 优化多语言国际化支持,确保后台管理界面在不同语言环境下的信息表达一致性。
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user