feat: 增加管理端多语言与多模块界面国际化支持
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
@@ -47,6 +48,7 @@ function inferPercentFrom(dim: 2 | 3 | 4, rows: OddsItemRow[], typeList: AdminPl
|
||||
}
|
||||
|
||||
export function RebateConfigDocScreen() {
|
||||
const { t } = useTranslation(["config", "common"]);
|
||||
const formatDt = useAdminDateTimeFormatter();
|
||||
const [types, setTypes] = useState<AdminPlayTypeRow[]>([]);
|
||||
const [listRows, setListRows] = useState<ConfigVersionSummary[]>([]);
|
||||
@@ -67,20 +69,20 @@ export function RebateConfigDocScreen() {
|
||||
const d = await getAdminPlayTypes();
|
||||
setTypes(d.items);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "加载玩法失败");
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
|
||||
setTypes([]);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const refreshList = useCallback(async () => {
|
||||
try {
|
||||
const d = await getAllConfigVersions(getOddsVersions);
|
||||
setListRows(d.items);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "加载版本失败");
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
|
||||
setListRows([]);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(async () => {
|
||||
@@ -105,13 +107,13 @@ export function RebateConfigDocScreen() {
|
||||
setP3(inferPercentFrom(3, rows, typeList));
|
||||
setP4(inferPercentFrom(4, rows, typeList));
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "加载明细失败");
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
|
||||
setDetail(null);
|
||||
setDraftRows([]);
|
||||
} finally {
|
||||
setLoadingDetail(false);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (listRows.length === 0 || selectedId !== "") {
|
||||
@@ -194,10 +196,10 @@ export function RebateConfigDocScreen() {
|
||||
setP2(inferPercentFrom(2, rows, types));
|
||||
setP3(inferPercentFrom(3, rows, types));
|
||||
setP4(inferPercentFrom(4, rows, types));
|
||||
toast.success("已保存草稿");
|
||||
toast.success(t("versionActions.saveDraft", { ns: "config" }));
|
||||
void refreshList();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "保存失败");
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("wallet.saveFailed", { ns: "config" }));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -216,11 +218,11 @@ export function RebateConfigDocScreen() {
|
||||
setP2(inferPercentFrom(2, rows, types));
|
||||
setP3(inferPercentFrom(3, rows, types));
|
||||
setP4(inferPercentFrom(4, rows, types));
|
||||
toast.success("已发布赔率版本(含回水)");
|
||||
toast.success("Published odds version with rebate");
|
||||
void refreshList();
|
||||
setSelectedId(String(d.id));
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "发布失败");
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "Publish failed");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -234,7 +236,7 @@ export function RebateConfigDocScreen() {
|
||||
reason: `rebate draft ${new Date().toISOString()}`,
|
||||
clone_from_version_id: active?.id ?? null,
|
||||
});
|
||||
toast.success(`已创建草稿 v${d.version_no}`);
|
||||
toast.success(`Created draft v${d.version_no}`);
|
||||
await refreshList();
|
||||
setSelectedId(String(d.id));
|
||||
const rows = d.items.map((it) => ({ ...it }));
|
||||
@@ -244,7 +246,7 @@ export function RebateConfigDocScreen() {
|
||||
setP3(inferPercentFrom(3, rows, types));
|
||||
setP4(inferPercentFrom(4, rows, types));
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "创建草稿失败");
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "Create draft failed");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -255,10 +257,10 @@ export function RebateConfigDocScreen() {
|
||||
async function handleDeleteVersion(row: ConfigVersionSummary) {
|
||||
try {
|
||||
await deleteOddsVersion(row.id);
|
||||
toast.success("已删除该版本");
|
||||
toast.success(t("versionSwitcher.delete", { ns: "config" }));
|
||||
await refreshList();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "删除失败");
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "Delete failed");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -266,7 +268,7 @@ export function RebateConfigDocScreen() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-lg">佣金 / 回水配置</CardTitle>
|
||||
<CardTitle className="text-lg">{t("nav.items.rebate", { ns: "config" })}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
@@ -275,8 +277,8 @@ export function RebateConfigDocScreen() {
|
||||
selectedId={selectedId}
|
||||
onSelectedIdChange={setSelectedId}
|
||||
loading={loading}
|
||||
sheetTitle="回水配置版本"
|
||||
sheetDescription="回水写入赔率版本草稿;选择与赔率配置共用同一套版本。"
|
||||
sheetTitle={`${t("nav.items.rebate", { ns: "config" })} ${t("versionSwitcher.sheetTitle", { ns: "config" })}`}
|
||||
sheetDescription="Rebate is stored in the odds draft version and shares the same version set as odds."
|
||||
onDeleteVersion={handleDeleteVersion}
|
||||
className="w-auto min-w-0"
|
||||
/>
|
||||
@@ -286,7 +288,7 @@ export function RebateConfigDocScreen() {
|
||||
loadingList={loading}
|
||||
loadingDetail={loadingDetail}
|
||||
saving={saving}
|
||||
publishLabel="发布生效"
|
||||
publishLabel="Publish"
|
||||
onRefresh={() => void refreshList()}
|
||||
onNewDraft={() => void handleNewDraft()}
|
||||
onSaveDraft={() => void handleSave()}
|
||||
@@ -295,9 +297,9 @@ export function RebateConfigDocScreen() {
|
||||
|
||||
{detail ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
编辑版本 v{detail.version_no} · {detail.status === "draft" ? "草稿" : detail.status === "active" ? "生效中" : "已归档"}
|
||||
Editing version v{detail.version_no} · {detail.status === "draft" ? "Draft" : detail.status === "active" ? "Active" : "Archived"}
|
||||
{!isDraft ? (
|
||||
<span className="text-amber-600 dark:text-amber-400"> — 请先新建草稿再改回水。</span>
|
||||
<span className="text-amber-600 dark:text-amber-400"> - Create a draft before editing rebate.</span>
|
||||
) : null}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -305,7 +307,7 @@ export function RebateConfigDocScreen() {
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<div className="grid gap-2">
|
||||
<Label>2D 回水率(%)</Label>
|
||||
<Label>2D Rebate Rate (%)</Label>
|
||||
{isDraft ? (
|
||||
<Input
|
||||
type="number"
|
||||
@@ -321,7 +323,7 @@ export function RebateConfigDocScreen() {
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>3D 回水率(%)</Label>
|
||||
<Label>3D Rebate Rate (%)</Label>
|
||||
{isDraft ? (
|
||||
<Input
|
||||
type="number"
|
||||
@@ -337,7 +339,7 @@ export function RebateConfigDocScreen() {
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>4D 回水率(%)</Label>
|
||||
<Label>4D Rebate Rate (%)</Label>
|
||||
{isDraft ? (
|
||||
<Input
|
||||
type="number"
|
||||
@@ -355,26 +357,26 @@ export function RebateConfigDocScreen() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 rounded-lg border bg-muted/30 p-4">
|
||||
<Checkbox id="win-enjoy" checked aria-disabled disabled aria-label="中奖是否享受回水" />
|
||||
<Checkbox id="win-enjoy" checked aria-disabled disabled aria-label="Apply rebate on winning tickets" />
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="win-enjoy" className="font-medium leading-snug">
|
||||
中奖是否享受回水
|
||||
Apply rebate on winning tickets
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
界面占位:后续可与风控 / 结算规则字段对齐并持久化。
|
||||
Placeholder field. It can later be aligned with risk and settlement rules and persisted.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1 text-sm">
|
||||
<span className="text-muted-foreground">生效时间(当前线上赔率版本)</span>
|
||||
<span className="text-muted-foreground">Effective Time (current active odds version)</span>
|
||||
<span className="font-mono text-sm">
|
||||
{activeHead?.effective_at ? formatDt(activeHead.effective_at) : "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{loading || loadingDetail ? (
|
||||
<p className="text-sm text-muted-foreground">加载中…</p>
|
||||
<p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user