refactor: 更新管理端页面元数据,统一国际化支持,移除冗余代码

This commit is contained in:
2026-05-21 17:27:52 +08:00
parent 26feed3c4f
commit e8a5507411
77 changed files with 1669 additions and 732 deletions

View File

@@ -61,7 +61,12 @@ function filterTypes(tab: CatTab, types: AdminPlayTypeRow[]): AdminPlayTypeRow[]
return types.filter((t) => t.dimension === dim);
}
export function OddsConfigDocScreen() {
type OddsConfigDocScreenProps = {
/** 嵌入「赔率与回水」合并页时去掉外层 ConfigDocPage */
embedded?: boolean;
};
export function OddsConfigDocScreen({ embedded = false }: OddsConfigDocScreenProps) {
const { t } = useTranslation(["config", "adminUsers", "common"]);
const formatDt = useAdminDateTimeFormatter();
const [types, setTypes] = useState<AdminPlayTypeRow[]>([]);
@@ -395,10 +400,7 @@ export function OddsConfigDocScreen() {
{ id: "d2", label: "2D" },
];
return (
<ConfigDocPage
title={t("nav.items.odds", { ns: "config" })}
filters={
const filtersBlock = (
<div className="space-y-4 rounded-xl border border-border/60 bg-card p-4">
<ConfigChipGroup label={t("odds.category", { ns: "config" })}>
{catTabs.map((tab) => (
@@ -427,8 +429,9 @@ export function OddsConfigDocScreen() {
)}
</ConfigChipGroup>
</div>
}
toolbar={
);
const toolbarBlock = (
<ConfigDocToolbar
switcher={
<ConfigVersionSwitcher
@@ -437,7 +440,7 @@ export function OddsConfigDocScreen() {
onSelectedIdChange={setSelectedId}
loading={loadingList}
sheetTitle={`${t("nav.items.odds", { ns: "config" })} ${t("versionSwitcher.sheetTitle", { ns: "config" })}`}
sheetDescription={t("odds.sheetDescription", { ns: "config" })}
sheetDescription={embedded ? undefined : t("odds.sheetDescription", { ns: "config" })}
onDeleteVersion={handleDeleteVersion}
onRollbackVersion={requestRollback}
rollbackBusy={saving}
@@ -456,29 +459,31 @@ export function OddsConfigDocScreen() {
/>
}
/>
}
context={
detail ? (
<ConfigContextBanner emphasis={!isDraft}>
{t("odds.activeVersionPrefix", { ns: "config" })}
{activeHead ? (
<>
v{activeHead.version_no}
{activeHead.effective_at ? ` · ${formatDt(activeHead.effective_at)}` : ""}
</>
) : (
"—"
)}
{!isDraft ? (
<>
{" "}
<ConfigContextEmphasis>{t("odds.readOnlyHint", { ns: "config" })}</ConfigContextEmphasis>
</>
) : null}
</ConfigContextBanner>
) : null
}
>
);
const contextBlock =
embedded || !detail ? null : (
<ConfigContextBanner emphasis={!isDraft}>
{t("odds.activeVersionPrefix", { ns: "config" })}
{activeHead ? (
<>
v{activeHead.version_no}
{activeHead.effective_at ? ` · ${formatDt(activeHead.effective_at)}` : ""}
</>
) : (
"—"
)}
{!isDraft ? (
<>
{" "}
<ConfigContextEmphasis>{t("odds.readOnlyHint", { ns: "config" })}</ConfigContextEmphasis>
</>
) : null}
</ConfigContextBanner>
);
const mainBlock = (
<>
{error ? <p className="text-sm text-destructive">{error}</p> : null}
{loadingDetail || loadingTypes ? (
@@ -489,7 +494,7 @@ export function OddsConfigDocScreen() {
<div className="grid min-h-[420px] gap-4 max-w-md">
{PRIZE_SCOPE_ORDER.map((scope) => {
const row = scopeRows[scope];
const hint = PRIZE_SCOPE_MULTIPLIER_HINT[scope];
const hint = embedded ? null : PRIZE_SCOPE_MULTIPLIER_HINT[scope];
const idx = row ? rowIndex(resolvedPlayCode, scope) : -1;
return (
<div key={scope} className="grid gap-1">
@@ -517,13 +522,15 @@ export function OddsConfigDocScreen() {
{row.odds_value}
</ConfigReadonlyValue>
)}
<span className="text-sm text-muted-foreground tabular-nums">
{t("odds.multiplier", {
ns: "config",
value: oddsMultiplierLabel(row.odds_value),
currency: row.currency_code,
})}
</span>
{!embedded ? (
<span className="text-sm text-muted-foreground tabular-nums">
{t("odds.multiplier", {
ns: "config",
value: oddsMultiplierLabel(row.odds_value),
currency: row.currency_code,
})}
</span>
) : null}
</div>
) : (
<p className="text-sm text-destructive">{t("odds.missingScopeRow", { ns: "config", scope })}</p>
@@ -547,11 +554,17 @@ export function OddsConfigDocScreen() {
{rebatePercentUi}
</ConfigReadonlyValue>
)}
<p className="text-sm text-muted-foreground">{t("odds.rebateRateHint", { ns: "config" })}</p>
{!embedded ? (
<p className="text-sm text-muted-foreground">{t("odds.rebateRateHint", { ns: "config" })}</p>
) : null}
</div>
</div>
) : null}
</>
);
const dialogs = (
<>
<Dialog open={rollbackOpen} onOpenChange={setRollbackOpen}>
<DialogContent showCloseButton className="sm:max-w-md">
<DialogHeader>
@@ -612,6 +625,30 @@ export function OddsConfigDocScreen() {
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
if (embedded) {
return (
<div className="space-y-6">
{filtersBlock}
{toolbarBlock}
{contextBlock}
{mainBlock}
{dialogs}
</div>
);
}
return (
<ConfigDocPage
title={t("nav.items.odds", { ns: "config" })}
filters={filtersBlock}
toolbar={toolbarBlock}
context={contextBlock}
>
{mainBlock}
{dialogs}
</ConfigDocPage>
);
}

View File

@@ -48,7 +48,11 @@ function inferPercentFrom(dim: 2 | 3 | 4, rows: OddsItemRow[], typeList: AdminPl
return hit ? rateToPercentUi(String(hit.rebate_rate)) : "0";
}
export function RebateConfigDocScreen() {
type RebateConfigDocScreenProps = {
embedded?: boolean;
};
export function RebateConfigDocScreen({ embedded = false }: RebateConfigDocScreenProps) {
const { t } = useTranslation(["config", "common"]);
const formatDt = useAdminDateTimeFormatter();
const [types, setTypes] = useState<AdminPlayTypeRow[]>([]);
@@ -266,60 +270,59 @@ export function RebateConfigDocScreen() {
}
}
return (
<ConfigDocPage
title={t("nav.items.rebate", { ns: "config" })}
toolbar={
<ConfigDocToolbar
switcher={
<ConfigVersionSwitcher
versions={listRows}
selectedId={selectedId}
onSelectedIdChange={setSelectedId}
loading={loading}
sheetTitle={`${t("nav.items.rebate", { ns: "config" })} ${t("versionSwitcher.sheetTitle", { ns: "config" })}`}
sheetDescription={t("rebate.sheetDescription", { ns: "config" })}
onDeleteVersion={handleDeleteVersion}
/>
}
actions={
<ConfigVersionActions
isDraft={isDraft}
loadingList={loading}
loadingDetail={loadingDetail}
saving={saving}
publishLabel={t("rebate.publishLabel", { ns: "config" })}
onRefresh={() => void refreshList()}
onNewDraft={() => void handleNewDraft()}
onSaveDraft={() => void handleSave()}
onPublish={() => void handlePublish()}
/>
}
const toolbarBlock = embedded ? null : (
<ConfigDocToolbar
switcher={
<ConfigVersionSwitcher
versions={listRows}
selectedId={selectedId}
onSelectedIdChange={setSelectedId}
loading={loading}
sheetTitle={`${t("nav.items.rebate", { ns: "config" })} ${t("versionSwitcher.sheetTitle", { ns: "config" })}`}
sheetDescription={t("rebate.sheetDescription", { ns: "config" })}
onDeleteVersion={handleDeleteVersion}
/>
}
context={
detail ? (
<ConfigContextBanner emphasis={!isDraft}>
{t("rebate.editingVersion", {
ns: "config",
version: detail.version_no,
status:
detail.status === "draft"
? t("versionStatus.draft", { ns: "config" })
: detail.status === "active"
? t("versionStatus.active", { ns: "config" })
: t("versionStatus.archived", { ns: "config" }),
})}
{!isDraft ? (
<>
{" "}
<ConfigContextEmphasis>{t("rebate.readOnlyHint", { ns: "config" })}</ConfigContextEmphasis>
</>
) : null}
</ConfigContextBanner>
) : null
actions={
<ConfigVersionActions
isDraft={isDraft}
loadingList={loading}
loadingDetail={loadingDetail}
saving={saving}
publishLabel={t("rebate.publishLabel", { ns: "config" })}
onRefresh={() => void refreshList()}
onNewDraft={() => void handleNewDraft()}
onSaveDraft={() => void handleSave()}
onPublish={() => void handlePublish()}
/>
}
>
/>
);
const contextBlock =
embedded || !detail ? null : (
<ConfigContextBanner emphasis={!isDraft}>
{t("rebate.editingVersion", {
ns: "config",
version: detail.version_no,
status:
detail.status === "draft"
? t("versionStatus.draft", { ns: "config" })
: detail.status === "active"
? t("versionStatus.active", { ns: "config" })
: t("versionStatus.archived", { ns: "config" }),
})}
{!isDraft ? (
<>
{" "}
<ConfigContextEmphasis>{t("rebate.readOnlyHint", { ns: "config" })}</ConfigContextEmphasis>
</>
) : null}
</ConfigContextBanner>
);
const fieldsBlock = (
<>
<div className="grid gap-5 sm:grid-cols-3">
<div className="grid gap-2">
<Label>{t("rebate.fields.d2", { ns: "config" })}</Label>
@@ -386,16 +389,37 @@ export function RebateConfigDocScreen() {
</div>
</div>
{!embedded ? (
<div className="grid gap-1 text-sm">
<span className="text-muted-foreground">{t("rebate.effectiveTime", { ns: "config" })}</span>
<span className="font-mono text-sm">
{activeHead?.effective_at ? formatDt(activeHead.effective_at) : "—"}
</span>
</div>
) : null}
{loading || loadingDetail ? (
<p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>
) : null}
</>
);
if (embedded) {
return (
<div className="space-y-6">
{contextBlock}
{fieldsBlock}
</div>
);
}
return (
<ConfigDocPage
title={t("nav.items.rebate", { ns: "config" })}
toolbar={toolbarBlock}
context={contextBlock}
>
{fieldsBlock}
</ConfigDocPage>
);
}