feat: 统一管理端多语言、配置与票据/结算页面重构
This commit is contained in:
@@ -284,7 +284,7 @@ export function PlayConfigDocScreen() {
|
||||
const payload = buildPlayConfigSavePayload(draftRows);
|
||||
for (const r of payload) {
|
||||
if (r.min_bet_amount > r.max_bet_amount) {
|
||||
toast.error(`${r.play_code}: min_bet_amount cannot exceed max_bet_amount`);
|
||||
toast.error(t("play.validation.minMaxInvalid", { ns: "config", playCode: r.play_code }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -315,7 +315,7 @@ export function PlayConfigDocScreen() {
|
||||
void refreshList();
|
||||
setSelectedId(String(d.id));
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "Publish failed");
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("play.publishFailed", { ns: "config" }));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -329,14 +329,14 @@ export function PlayConfigDocScreen() {
|
||||
reason: `draft ${new Date().toISOString()}`,
|
||||
clone_from_version_id: active?.id ?? null,
|
||||
});
|
||||
toast.success(`Created draft v${d.version_no}`);
|
||||
toast.success(t("play.createDraftSuccess", { ns: "config", version: d.version_no }));
|
||||
setCreatingDraftId(String(d.id));
|
||||
setSelectedId(String(d.id));
|
||||
setDetail(d);
|
||||
setDraftRows(d.items.map((it) => ({ ...it })));
|
||||
void refreshList();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "Create draft failed");
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("play.createDraftFailed", { ns: "config" }));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -356,7 +356,7 @@ export function PlayConfigDocScreen() {
|
||||
updateConfigRow(rulePlayCode, { rule_text_zh: ruleDraftZh.trim() || null });
|
||||
setRuleDialogOpen(false);
|
||||
setRulePlayCode(null);
|
||||
toast.message("Rule text saved into the local draft. Save the draft to persist it.");
|
||||
toast.message(t("play.ruleSavedLocal", { ns: "config" }));
|
||||
}
|
||||
|
||||
const activeHead = list.find((x) => x.status === "active");
|
||||
@@ -367,7 +367,7 @@ export function PlayConfigDocScreen() {
|
||||
toast.success(t("versionSwitcher.delete", { ns: "config" }));
|
||||
await refreshList();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "Delete failed");
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("play.deleteFailed", { ns: "config" }));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -407,14 +407,14 @@ export function PlayConfigDocScreen() {
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{activeHead ? (
|
||||
<>
|
||||
Active version v{activeHead.version_no}
|
||||
{t("play.activeVersion", { ns: "config", version: activeHead.version_no })}
|
||||
{activeHead.effective_at ? ` · ${activeHead.effective_at}` : ""}
|
||||
</>
|
||||
) : null}
|
||||
{!isDraft ? (
|
||||
<span className="text-amber-600 dark:text-amber-400">
|
||||
{activeHead ? " — " : ""}
|
||||
Limits and rules are read-only. Create a draft first.
|
||||
{t("play.readOnlyHint", { ns: "config" })}
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
@@ -424,14 +424,14 @@ export function PlayConfigDocScreen() {
|
||||
<div className="rounded-xl border bg-muted/20 p-3">
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Batch switches</p>
|
||||
<p className="text-sm font-medium">{t("play.batchSwitchesTitle", { ns: "config" })}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Only updates the current draft. The player betting table refreshes after save and publish.
|
||||
{t("play.batchSwitchesDesc", { ns: "config" })}
|
||||
</p>
|
||||
</div>
|
||||
{!isDraft ? (
|
||||
<span className="text-xs text-amber-600 dark:text-amber-400">
|
||||
Current version is read-only. Create a draft first.
|
||||
{t("play.readOnlyDraftHint", { ns: "config" })}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -444,7 +444,13 @@ export function PlayConfigDocScreen() {
|
||||
<div className="min-w-[92px]">
|
||||
<p className="text-sm font-medium">{group.label}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{group.total > 0 ? `${group.enabledCount}/${group.total} enabled` : "No play types"}
|
||||
{group.total > 0
|
||||
? t("play.batchEnabledCount", {
|
||||
ns: "config",
|
||||
enabledCount: group.enabledCount,
|
||||
total: group.total,
|
||||
})
|
||||
: t("play.noPlayTypes", { ns: "config" })}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -454,7 +460,9 @@ export function PlayConfigDocScreen() {
|
||||
disabled={!isDraft || saving || group.total === 0}
|
||||
onClick={() => applyBatchSwitch(group, !group.allEnabled)}
|
||||
>
|
||||
{group.allEnabled ? "Disable" : "Enable"}
|
||||
{group.allEnabled
|
||||
? t("play.actions.disable", { ns: "config" })
|
||||
: t("play.actions.enable", { ns: "config" })}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
@@ -471,14 +479,14 @@ export function PlayConfigDocScreen() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-center">Play Code</TableHead>
|
||||
<TableHead className="w-[100px] text-center">Category</TableHead>
|
||||
<TableHead className="w-[88px] text-center">Status</TableHead>
|
||||
<TableHead className="min-w-[120px] text-center">Display Name</TableHead>
|
||||
<TableHead className="w-[120px] text-center">Order</TableHead>
|
||||
<TableHead className="w-[110px] text-center">Min Bet</TableHead>
|
||||
<TableHead className="w-[110px] text-center">Max Bet</TableHead>
|
||||
<TableHead className="w-[140px] text-center">Actions</TableHead>
|
||||
<TableHead className="text-center">{t("play.table.playCode", { ns: "config" })}</TableHead>
|
||||
<TableHead className="w-[100px] text-center">{t("play.table.category", { ns: "config" })}</TableHead>
|
||||
<TableHead className="w-[88px] text-center">{t("play.table.status", { ns: "config" })}</TableHead>
|
||||
<TableHead className="min-w-[120px] text-center">{t("play.table.displayName", { ns: "config" })}</TableHead>
|
||||
<TableHead className="w-[120px] text-center">{t("play.table.order", { ns: "config" })}</TableHead>
|
||||
<TableHead className="w-[110px] text-center">{t("play.table.minBet", { ns: "config" })}</TableHead>
|
||||
<TableHead className="w-[110px] text-center">{t("play.table.maxBet", { ns: "config" })}</TableHead>
|
||||
<TableHead className="w-[140px] text-center">{t("play.table.actions", { ns: "config" })}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -494,11 +502,13 @@ export function PlayConfigDocScreen() {
|
||||
onCheckedChange={(v) => {
|
||||
updateConfigRow(row.play_code, { is_enabled: v === true });
|
||||
}}
|
||||
aria-label={`Enable ${row.play_code}`}
|
||||
aria-label={t("play.aria.enablePlay", { ns: "config", playCode: row.play_code })}
|
||||
/>
|
||||
) : (
|
||||
<ConfigReadonlyValue className="justify-center">
|
||||
{row.is_enabled ? "Enabled" : "Disabled"}
|
||||
{row.is_enabled
|
||||
? t("play.states.enabled", { ns: "config" })
|
||||
: t("play.states.disabled", { ns: "config" })}
|
||||
</ConfigReadonlyValue>
|
||||
)}
|
||||
</TableCell>
|
||||
@@ -588,10 +598,10 @@ export function PlayConfigDocScreen() {
|
||||
disabled={saving}
|
||||
onClick={() => openRuleEditor(row.play_code)}
|
||||
>
|
||||
Rule Text
|
||||
{t("play.actions.ruleText", { ns: "config" })}
|
||||
</Button>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">Read only</span>
|
||||
<span className="text-sm text-muted-foreground">{t("play.states.readOnly", { ns: "config" })}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -605,13 +615,13 @@ export function PlayConfigDocScreen() {
|
||||
<Dialog open={ruleDialogOpen} onOpenChange={setRuleDialogOpen}>
|
||||
<DialogContent showCloseButton className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rule Text (Chinese)</DialogTitle>
|
||||
<DialogTitle>{t("play.ruleDialog.title", { ns: "config" })}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Play {rulePlayCode ?? "—"}; changes are only stored in the draft until you save and publish it.
|
||||
{t("play.ruleDialog.description", { ns: "config", playCode: rulePlayCode ?? "—" })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="rule-zh">rule_text_zh</Label>
|
||||
<Label htmlFor="rule-zh">{t("play.ruleDialog.fieldLabel", { ns: "config" })}</Label>
|
||||
<textarea
|
||||
id="rule-zh"
|
||||
className="border-input bg-background ring-ring/24 focus-visible:ring-[3px] min-h-[140px] w-full rounded-lg border px-3 py-2 text-sm outline-none"
|
||||
@@ -624,7 +634,7 @@ export function PlayConfigDocScreen() {
|
||||
{t("actions.cancel", { ns: "adminUsers" })}
|
||||
</Button>
|
||||
<Button type="button" onClick={saveRuleZh}>
|
||||
Apply to Draft
|
||||
{t("play.ruleDialog.apply", { ns: "config" })}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user