feat(admin): enhance operations guidance and session handling
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
This commit is contained in:
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user