feat(admin): enhance operations guidance and session handling
Some checks failed
lotteryadmin CI / build (push) Has been cancelled

This commit is contained in:
wchino
2026-07-22 20:53:43 +08:00
parent 98a3d0b68e
commit 85ad369cf9
55 changed files with 1477 additions and 342 deletions

View File

@@ -15,6 +15,8 @@ import {
putOddsItems,
} from "@/api/admin-config";
import { getAdminBetProviders } from "@/api/admin-bet-providers";
import { AdminHelpText } from "@/components/admin/admin-field-label";
import { AdminPageGuideDialog } from "@/components/admin/admin-page-guide-dialog";
import { Button } from "@/components/ui/button";
import { ConfigChip, ConfigChipGroup } from "@/modules/config/config-chip-group";
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
@@ -28,7 +30,6 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { ratioToPercentUi } from "@/lib/admin-rate-percent";
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
@@ -602,7 +603,20 @@ export function OddsConfigDocScreen({
const filtersInner = (
<>
<ConfigChipGroup label={t("betProviders.title", { ns: "config", defaultValue: "开注商" })}>
<ConfigChipGroup
label={
<AdminHelpText
helpId="odds-provider"
helpText={t("odds.help.fields.provider", { ns: "config" })}
helpAriaLabel={t("odds.help.aria", {
ns: "config",
field: t("betProviders.title", { ns: "config", defaultValue: "开注商" }),
})}
>
{t("betProviders.title", { ns: "config", defaultValue: "开注商" })}
</AdminHelpText>
}
>
<ConfigChip
active={providerCode === GLOBAL_PROVIDER_CODE}
onClick={() => setProviderCode(GLOBAL_PROVIDER_CODE)}
@@ -619,7 +633,20 @@ export function OddsConfigDocScreen({
</ConfigChip>
))}
</ConfigChipGroup>
<ConfigChipGroup label={t("odds.category", { ns: "config" })}>
<ConfigChipGroup
label={
<AdminHelpText
helpId="odds-category"
helpText={t("odds.help.fields.category", { ns: "config" })}
helpAriaLabel={t("odds.help.aria", {
ns: "config",
field: t("odds.category", { ns: "config" }),
})}
>
{t("odds.category", { ns: "config" })}
</AdminHelpText>
}
>
{catTabs.map((tab) => (
<ConfigChip
key={tab.id}
@@ -654,7 +681,20 @@ export function OddsConfigDocScreen({
)}
</div>
) : (
<ConfigChipGroup label={t("odds.playType", { ns: "config" })}>
<ConfigChipGroup
label={
<AdminHelpText
helpId="odds-play-type"
helpText={t("odds.help.fields.playType", { ns: "config" })}
helpAriaLabel={t("odds.help.aria", {
ns: "config",
field: t("odds.playType", { ns: "config" }),
})}
>
{t("odds.playType", { ns: "config" })}
</AdminHelpText>
}
>
{filteredTypes.length === 0 ? (
<span className="text-sm text-muted-foreground">{t("odds.noPlayTypes", { ns: "config" })}</span>
) : (
@@ -705,18 +745,63 @@ export function OddsConfigDocScreen({
/>
}
actions={
<ConfigVersionActions
isDraft={isDraft}
canManage={canManage}
loadingList={resolvedLoadingList}
loadingDetail={resolvedLoadingDetail}
saving={saving}
suppressDraftActions={mergedLayout && isDraft}
onRefresh={() => void refreshList()}
onNewDraft={() => void handleNewDraft()}
onSaveDraft={() => void handleSave()}
onPublish={() => void requestPublishConfirm()}
/>
<>
<AdminPageGuideDialog
triggerLabel={t("odds.help.openGuide", { ns: "config" })}
title={t("odds.help.guideTitle", { ns: "config" })}
description={t("odds.help.guideDescription", { ns: "config" })}
sections={[
{
title: t("odds.help.guide.scopeTitle", { ns: "config" }),
description: t("odds.help.guide.scopeDescription", { ns: "config" }),
items: [
{
label: t("betProviders.title", { ns: "config" }),
description: t("odds.help.fields.provider", { ns: "config" }),
},
{
label: t("odds.category", { ns: "config" }),
description: t("odds.help.fields.category", { ns: "config" }),
},
{
label: t("odds.playType", { ns: "config" }),
description: t("odds.help.fields.playType", { ns: "config" }),
},
],
},
{
title: t("odds.help.guide.settingsTitle", { ns: "config" }),
description: t("odds.help.guide.settingsDescription", { ns: "config" }),
items: [
{
label: t("odds.table.multiplier", { ns: "config" }),
description: t("odds.help.fields.multiplier", { ns: "config" }),
},
{
label: t("odds.rebateRate", { ns: "config" }),
description: t("odds.help.fields.rebateRate", { ns: "config" }),
},
],
},
{
title: t("odds.help.guide.workflowTitle", { ns: "config" }),
description: t("odds.help.guide.workflowDescription", { ns: "config" }),
},
]}
/>
<ConfigVersionActions
isDraft={isDraft}
canManage={canManage}
loadingList={resolvedLoadingList}
loadingDetail={resolvedLoadingDetail}
saving={saving}
suppressDraftActions={mergedLayout && isDraft}
onRefresh={() => void refreshList()}
onNewDraft={() => void handleNewDraft()}
onSaveDraft={() => void handleSave()}
onPublish={() => void requestPublishConfirm()}
/>
</>
}
/>
);
@@ -743,10 +828,21 @@ export function OddsConfigDocScreen({
{scopeEditorRows.map(({ scope, row, hint, sourceLabel, sourceClassName }) => (
<div key={scope} className="grid min-w-0 gap-2 border border-border/60 bg-muted/20 p-3">
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
<Label className="text-xs font-medium text-muted-foreground">
<AdminHelpText
helpId={`odds-multiplier-${providerCode}-${resolvedPlayCode}-${scope}`}
helpText={t("odds.help.fields.scopeMultiplier", {
ns: "config",
scope: prizeScopeLabel(scope, t),
})}
helpAriaLabel={t("odds.help.aria", {
ns: "config",
field: prizeScopeLabel(scope, t),
})}
className="text-xs font-medium text-muted-foreground"
>
{prizeScopeLabel(scope, t)}
{hint ? <span className="ml-1 font-normal">{hint}</span> : null}
</Label>
</AdminHelpText>
{row ? (
<span className={cn("rounded-full border px-1.5 py-0.5 text-[11px] font-semibold", sourceClassName)}>
{sourceLabel}
@@ -779,9 +875,17 @@ export function OddsConfigDocScreen({
</div>
))}
<div className="grid min-w-0 gap-2 border border-border/60 bg-muted/20 p-3">
<Label className="truncate text-xs font-medium text-muted-foreground">
<AdminHelpText
helpId={`odds-rebate-${providerCode}-${resolvedPlayCode}`}
helpText={t("odds.help.fields.rebateRate", { ns: "config" })}
helpAriaLabel={t("odds.help.aria", {
ns: "config",
field: t("odds.rebateRate", { ns: "config" }),
})}
className="text-xs font-medium text-muted-foreground"
>
{t("odds.rebateRate", { ns: "config" })}
</Label>
</AdminHelpText>
{canEditDraft ? (
<Input
type="text"
@@ -895,7 +999,20 @@ export function OddsConfigDocScreen({
{toolbarBlock}
<div className="grid gap-0 overflow-hidden rounded-lg border border-border/60 lg:grid-cols-[minmax(0,17rem)_minmax(0,1fr)]">
<aside className="space-y-4 border-b border-border/50 bg-muted/20 px-4 py-4 lg:border-r lg:border-b-0">
<ConfigChipGroup label={t("betProviders.title", { ns: "config", defaultValue: "开注商" })}>
<ConfigChipGroup
label={
<AdminHelpText
helpId="odds-provider-merged"
helpText={t("odds.help.fields.provider", { ns: "config" })}
helpAriaLabel={t("odds.help.aria", {
ns: "config",
field: t("betProviders.title", { ns: "config", defaultValue: "开注商" }),
})}
>
{t("betProviders.title", { ns: "config", defaultValue: "开注商" })}
</AdminHelpText>
}
>
<ConfigChip
active={providerCode === GLOBAL_PROVIDER_CODE}
onClick={() => setProviderCode(GLOBAL_PROVIDER_CODE)}

View File

@@ -2,6 +2,7 @@
import { useTranslation } from "react-i18next";
import { AdminHelpText } from "@/components/admin/admin-field-label";
import { ConfigChip, ConfigChipGroup } from "@/modules/config/config-chip-group";
import {
Select,
@@ -60,7 +61,17 @@ export function OddsConfigPlayNav({
return (
<div className={cn("space-y-4", className)}>
<ConfigChipGroup label={t("odds.category")}>
<ConfigChipGroup
label={
<AdminHelpText
helpId="odds-nav-category"
helpText={t("odds.help.fields.category")}
helpAriaLabel={t("odds.help.aria", { field: t("odds.category") })}
>
{t("odds.category")}
</AdminHelpText>
}
>
{catTabs.map((tab) => (
<ConfigChip
key={tab.id}
@@ -73,7 +84,14 @@ export function OddsConfigPlayNav({
</ConfigChipGroup>
<div className="space-y-1.5">
<p className="text-sm font-medium">{t("odds.playType")}</p>
<AdminHelpText
helpId="odds-nav-play-type"
helpText={t("odds.help.fields.playType")}
helpAriaLabel={t("odds.help.aria", { field: t("odds.playType") })}
className="text-sm font-medium"
>
{t("odds.playType")}
</AdminHelpText>
<Select
modal={false}
value={playSelectValue}

View File

@@ -19,6 +19,8 @@ import { ConfigChipGroup } from "@/modules/config/config-chip-group";
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
import { ConfirmableSwitch } from "@/components/admin/confirmable-switch";
import { AdminHelpText } from "@/components/admin/admin-field-label";
import { AdminPageGuideDialog } from "@/components/admin/admin-page-guide-dialog";
import {
Dialog,
DialogContent,
@@ -489,25 +491,68 @@ export function PlayConfigDocScreen() {
/>
}
actions={
<ConfigVersionActions
isDraft={isDraft}
canManage={canManage}
loadingList={loadingList}
loadingDetail={loadingDetail}
saving={saving}
onRefresh={() => void refreshList()}
onNewDraft={() => void handleNewDraft()}
onSaveDraft={() => void handleSaveDraft()}
onPublish={() =>
requestConfirm({
title: t("play.publishDialog.title", { ns: "config" }),
description: t("play.publishDialog.description", { ns: "config" }),
confirmLabel: t("play.publishDialog.confirm", { ns: "config" }),
confirmVariant: "destructive",
onConfirm: () => handlePublish(),
})
}
/>
<>
<AdminPageGuideDialog
triggerLabel={t("play.help.openGuide", { ns: "config" })}
title={t("play.help.guideTitle", { ns: "config" })}
description={t("play.help.guideDescription", { ns: "config" })}
sections={[
{
title: t("play.help.guide.settingsTitle", { ns: "config" }),
description: t("play.help.guide.settingsDescription", { ns: "config" }),
items: [
{
label: t("play.table.status", { ns: "config" }),
description: t("play.help.fields.status", { ns: "config" }),
},
{
label: t("play.table.displayName", { ns: "config" }),
description: t("play.help.fields.displayName", { ns: "config" }),
},
{
label: t("play.table.order", { ns: "config" }),
description: t("play.help.fields.order", { ns: "config" }),
},
{
label: t("play.table.minBet", { ns: "config" }),
description: t("play.help.fields.minBet", { ns: "config" }),
},
{
label: t("play.table.maxBet", { ns: "config" }),
description: t("play.help.fields.maxBet", { ns: "config" }),
},
],
},
{
title: t("play.help.guide.batchTitle", { ns: "config" }),
description: t("play.help.guide.batchDescription", { ns: "config" }),
},
{
title: t("play.help.guide.workflowTitle", { ns: "config" }),
description: t("play.help.guide.workflowDescription", { ns: "config" }),
},
]}
/>
<ConfigVersionActions
isDraft={isDraft}
canManage={canManage}
loadingList={loadingList}
loadingDetail={loadingDetail}
saving={saving}
onRefresh={() => void refreshList()}
onNewDraft={() => void handleNewDraft()}
onSaveDraft={() => void handleSaveDraft()}
onPublish={() =>
requestConfirm({
title: t("play.publishDialog.title", { ns: "config" }),
description: t("play.publishDialog.description", { ns: "config" }),
confirmLabel: t("play.publishDialog.confirm", { ns: "config" }),
confirmVariant: "destructive",
onConfirm: () => handlePublish(),
})
}
/>
</>
}
/>
}
@@ -578,17 +623,43 @@ export function PlayConfigDocScreen() {
</div>
{isDraft ? (
<div className="space-y-1.5">
<ConfigChipGroup label={t("play.batchSwitchesTitle", { ns: "config" })}>
<ConfigChipGroup
label={
<AdminHelpText
helpId="play-batch-switches"
helpText={t("play.help.fields.batchSwitches", { ns: "config" })}
helpAriaLabel={t("play.help.aria", {
ns: "config",
field: t("play.batchSwitchesTitle", { ns: "config" }),
})}
>
{t("play.batchSwitchesTitle", { ns: "config" })}
</AdminHelpText>
}
>
{batchSwitchStates.map((group) => {
const groupOn = group.allEnabled;
const isPartial =
group.total > 0 && group.enabledCount > 0 && group.enabledCount < group.total;
return (
<label
<div
key={group.key}
className="inline-flex cursor-pointer items-center gap-2 rounded-md border border-border/60 px-2.5 py-1.5 text-sm"
className="inline-flex items-center gap-2 rounded-md border border-border/60 px-2.5 py-1.5 text-sm"
>
<span>{group.label}</span>
<AdminHelpText
helpId={`play-batch-${group.key}`}
helpText={t("play.help.fields.batchGroup", {
ns: "config",
group: group.label,
count: group.total,
})}
helpAriaLabel={t("play.help.aria", {
ns: "config",
field: group.label,
})}
>
{group.label}
</AdminHelpText>
<span className="text-xs tabular-nums text-muted-foreground">
{t("play.batchEnabledCount", {
ns: "config",
@@ -622,7 +693,7 @@ export function PlayConfigDocScreen() {
});
}}
/>
</label>
</div>
);
})}
</ConfigChipGroup>
@@ -644,11 +715,71 @@ export function PlayConfigDocScreen() {
<TableRow>
<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="w-36 text-center">{t("play.table.displayName", { ns: "config" })}</TableHead>
<TableHead className="w-24 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-[88px] text-center">
<AdminHelpText
helpId="play-status"
helpText={t("play.help.fields.status", { ns: "config" })}
helpAriaLabel={t("play.help.aria", {
ns: "config",
field: t("play.table.status", { ns: "config" }),
})}
className="justify-center"
>
{t("play.table.status", { ns: "config" })}
</AdminHelpText>
</TableHead>
<TableHead className="w-36 text-center">
<AdminHelpText
helpId="play-display-name"
helpText={t("play.help.fields.displayName", { ns: "config" })}
helpAriaLabel={t("play.help.aria", {
ns: "config",
field: t("play.table.displayName", { ns: "config" }),
})}
className="justify-center"
>
{t("play.table.displayName", { ns: "config" })}
</AdminHelpText>
</TableHead>
<TableHead className="w-24 text-center">
<AdminHelpText
helpId="play-display-order"
helpText={t("play.help.fields.order", { ns: "config" })}
helpAriaLabel={t("play.help.aria", {
ns: "config",
field: t("play.table.order", { ns: "config" }),
})}
className="justify-center"
>
{t("play.table.order", { ns: "config" })}
</AdminHelpText>
</TableHead>
<TableHead className="w-[110px] text-center">
<AdminHelpText
helpId="play-min-bet"
helpText={t("play.help.fields.minBet", { ns: "config" })}
helpAriaLabel={t("play.help.aria", {
ns: "config",
field: t("play.table.minBet", { ns: "config" }),
})}
className="justify-center"
>
{t("play.table.minBet", { ns: "config" })}
</AdminHelpText>
</TableHead>
<TableHead className="w-[110px] text-center">
<AdminHelpText
helpId="play-max-bet"
helpText={t("play.help.fields.maxBet", { ns: "config" })}
helpAriaLabel={t("play.help.aria", {
ns: "config",
field: t("play.table.maxBet", { ns: "config" }),
})}
className="justify-center"
>
{t("play.table.maxBet", { ns: "config" })}
</AdminHelpText>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>