feat(ui): enhance table and admin components with improved layout and status display

- Updated global CSS to center-align table headers and cells, ensuring a consistent layout.
- Modified admin table components to replace switches with status badges for better clarity.
- Enhanced internationalization support by adding new strings for version actions and validation messages in multiple locales.
- Refactored configuration document screens to include version selection and improved user feedback on status changes.
This commit is contained in:
2026-05-26 11:13:16 +08:00
parent 05fa0cbeec
commit 4080f0b601
38 changed files with 788 additions and 608 deletions

View File

@@ -10,16 +10,18 @@ import {
getPlayConfigVersion,
getPlayConfigVersions,
postPlayConfigVersion,
patchAdminPlayType,
publishPlayConfigVersion,
putPlayConfigItems,
} from "@/api/admin-config";
import { Button } from "@/components/ui/button";
import { ConfigChipGroup } from "@/modules/config/config-chip-group";
import { ConfigContextBanner, ConfigContextEmphasis } from "@/modules/config/config-context-banner";
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
import {
ConfigVersionToolbarMeta,
ConfigVersionToolbarMetaEmphasis,
} from "@/modules/config/config-version-toolbar-meta";
import { ConfigSection } from "@/modules/config/config-section";
import { Switch } from "@/components/ui/switch";
import { ConfirmableSwitch } from "@/components/admin/confirmable-switch";
import {
Dialog,
DialogContent,
@@ -136,7 +138,7 @@ function buildPlayConfigSavePayload(
export function PlayConfigDocScreen() {
const { t } = useTranslation(["config", "adminUsers", "common"]);
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const { request: requestConfirm, ConfirmDialog, busy: confirmBusy } = useConfirmAction();
const profile = useAdminProfile();
const canManage = adminHasAnyPermission(profile?.permissions, [PRD_PLAY_SWITCH_MANAGE]);
const formatDt = useAdminDateTimeFormatter();
@@ -269,25 +271,10 @@ export function PlayConfigDocScreen() {
setDraftRows((prev) => prev.map((r) => (r.play_code === playCode ? { ...r, ...patch } : r)));
}
async function applyPlayToggleInstant(playCode: string, enabled: boolean) {
try {
await patchAdminPlayType(playCode, { is_enabled: enabled });
} catch (e) {
toast.error(
e instanceof LotteryApiBizError ? e.message : t("play.toggleInstantFailed", { ns: "config" }),
);
throw e;
}
}
async function applyBatchSwitch(group: PlayBatchSwitchGroup, enabled: boolean) {
const targets = draftRows.filter(group.match);
function applyBatchSwitch(group: PlayBatchSwitchGroup, enabled: boolean) {
setDraftRows((prev) =>
prev.map((row) => (group.match(row) ? { ...row, is_enabled: enabled } : row)),
);
for (const row of targets) {
await applyPlayToggleInstant(row.play_code, enabled);
}
}
const batchSwitchStates = useMemo(
@@ -448,26 +435,27 @@ export function PlayConfigDocScreen() {
}
/>
}
footer={
detail ? (
<ConfigVersionToolbarMeta emphasis={!isDraft}>
{activeHead ? (
<span>
{t("play.activeVersion", { ns: "config", version: activeHead.version_no })}
{activeHead.effective_at ? ` · ${formatDt(activeHead.effective_at)}` : ""}
</span>
) : null}
{!isDraft ? (
<ConfigVersionToolbarMetaEmphasis>
{t("play.readOnlyHint", { ns: "config" })}
</ConfigVersionToolbarMetaEmphasis>
) : activeHead ? (
<span>{t("versionToolbar.draftEditing", { ns: "config" })}</span>
) : null}
</ConfigVersionToolbarMeta>
) : null
}
/>
}
context={
detail ? (
<ConfigContextBanner emphasis={!isDraft}>
{activeHead ? (
<>
{t("play.activeVersion", { ns: "config", version: activeHead.version_no })}
{activeHead.effective_at ? ` · ${formatDt(activeHead.effective_at)}` : ""}
</>
) : null}
{!isDraft ? (
<>
{activeHead ? " — " : ""}
<ConfigContextEmphasis>{t("play.readOnlyHint", { ns: "config" })}</ConfigContextEmphasis>
</>
) : null}
</ConfigContextBanner>
) : null
}
>
{detail ? (
<ConfigSection
@@ -476,7 +464,9 @@ export function PlayConfigDocScreen() {
>
<ConfigChipGroup>
{batchSwitchStates.map((group) => {
const groupOn = group.enabledCount > 0;
const groupOn = group.allEnabled;
const isPartial =
group.total > 0 && group.enabledCount > 0 && group.enabledCount < group.total;
return (
<div
key={group.key}
@@ -486,16 +476,23 @@ export function PlayConfigDocScreen() {
<p className="text-sm font-medium text-foreground">{group.label}</p>
<p className="text-sm text-muted-foreground">
{group.total > 0
? t("play.batchEnabledCount", {
ns: "config",
enabledCount: group.enabledCount,
total: group.total,
})
? isPartial
? t("play.batchPartialEnabled", {
ns: "config",
enabledCount: group.enabledCount,
total: group.total,
})
: t("play.batchEnabledCount", {
ns: "config",
enabledCount: group.enabledCount,
total: group.total,
})
: t("play.noPlayTypes", { ns: "config" })}
</p>
</div>
<Switch
<ConfirmableSwitch
checked={groupOn}
confirmBusy={confirmBusy}
disabled={!isDraft || saving || group.total === 0}
aria-label={t("play.aria.batchGroupSwitch", {
ns: "config",
@@ -537,8 +534,8 @@ export function PlayConfigDocScreen() {
<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-32 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-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-[140px] text-center">{t("play.table.actions", { ns: "config" })}</TableHead>
@@ -550,41 +547,47 @@ export function PlayConfigDocScreen() {
<TableCell className="text-center font-mono text-sm">{row.play_code}</TableCell>
<TableCell className="text-center text-muted-foreground text-sm">{row.category ?? "—"}</TableCell>
<TableCell className="text-center">
<div className="flex justify-center">
<Switch
checked={row.is_enabled}
disabled={!isDraft || saving}
aria-label={t("play.aria.enablePlay", { ns: "config", playCode: row.play_code })}
onCheckedChange={(checked) => {
if (!isDraft) {
return;
}
const enabled = checked;
const action = enabled
? t("play.toggleEnable", { ns: "config" })
: t("play.toggleDisable", { ns: "config" });
requestConfirm({
title: t("play.toggleConfirmTitle", {
ns: "config",
action,
playCode: row.play_code,
}),
description: t("play.toggleConfirmDescription", { ns: "config" }),
confirmVariant: enabled ? "default" : "destructive",
onConfirm: () => {
updateConfigRow(row.play_code, { is_enabled: enabled });
void applyPlayToggleInstant(row.play_code, enabled);
},
});
}}
/>
</div>
{isDraft ? (
<div className="flex justify-center">
<ConfirmableSwitch
checked={row.is_enabled}
confirmBusy={confirmBusy}
disabled={saving}
aria-label={t("play.aria.enablePlay", { ns: "config", playCode: row.play_code })}
onCheckedChange={(enabled) => {
const action = enabled
? t("play.toggleEnable", { ns: "config" })
: t("play.toggleDisable", { ns: "config" });
requestConfirm({
title: t("play.toggleConfirmTitle", {
ns: "config",
action,
playCode: row.play_code,
}),
description: t("play.toggleConfirmDescription", { ns: "config" }),
confirmVariant: enabled ? "default" : "destructive",
onConfirm: () => {
updateConfigRow(row.play_code, { is_enabled: enabled });
},
});
}}
/>
</div>
) : (
<div className="flex justify-center">
<AdminStatusBadge status={row.is_enabled ? "enabled" : "disabled"}>
{row.is_enabled
? t("play.states.enabled", { ns: "config" })
: t("play.states.disabled", { ns: "config" })}
</AdminStatusBadge>
</div>
)}
</TableCell>
<TableCell className="w-32 text-center">
<TableCell className="w-36 text-center">
{isDraft ? (
<Input
type="text"
className="mx-auto h-8 w-28 text-center text-sm"
className="mx-auto h-8 w-full max-w-[9rem] text-center text-sm"
disabled={saving}
value={row.display_name ?? ""}
placeholder={row.play_code}
@@ -604,12 +607,12 @@ export function PlayConfigDocScreen() {
</ConfigReadonlyValue>
)}
</TableCell>
<TableCell className="w-[96px] text-center">
<TableCell className="w-24 text-center">
{isDraft ? (
<Input
type="text"
inputMode="numeric"
className="h-8 w-16 font-mono tabular-nums text-center"
className="mx-auto h-8 w-16 font-mono tabular-nums text-center"
value={row.display_order}
disabled={saving}
onChange={(e) => {
@@ -630,7 +633,7 @@ export function PlayConfigDocScreen() {
<Input
type="text"
inputMode="decimal"
className="h-8 text-center font-mono tabular-nums"
className="mx-auto h-8 w-24 text-center font-mono tabular-nums"
disabled={saving}
value={formatAdminMinorDecimal(row.min_bet_amount, amountCurrencyCode)}
onChange={(e) =>
@@ -651,7 +654,7 @@ export function PlayConfigDocScreen() {
<Input
type="text"
inputMode="decimal"
className="h-8 text-center font-mono tabular-nums"
className="mx-auto h-8 w-24 text-center font-mono tabular-nums"
disabled={saving}
value={formatAdminMinorDecimal(row.max_bet_amount, amountCurrencyCode)}
onChange={(e) =>