refactor(config): 优化版本切换抽屉与玩法配置输入展示

This commit is contained in:
2026-05-16 11:02:38 +08:00
parent 72a480ccc8
commit 34f9175304
2 changed files with 155 additions and 103 deletions

View File

@@ -13,13 +13,6 @@ import {
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
@@ -45,10 +38,6 @@ function versionStatusLabel(status: string): string {
return status; return status;
} }
function versionSelectLabel(v: ConfigVersionSummary): string {
return `#${v.id} · v${v.version_no} · ${versionStatusLabel(v.status)}`;
}
const STATUS_ORDER = ["draft", "active", "archived"] as const; const STATUS_ORDER = ["draft", "active", "archived"] as const;
export type ConfigVersionSwitcherProps = { export type ConfigVersionSwitcherProps = {
@@ -101,6 +90,11 @@ export function ConfigVersionSwitcher({
return groups; return groups;
}, [sortedVersions]); }, [sortedVersions]);
const selectedVersion = useMemo(
() => sortedVersions.find((v) => String(v.id) === selectedId) ?? null,
[selectedId, sortedVersions],
);
const statusCounts = useMemo( const statusCounts = useMemo(
() => () =>
STATUS_ORDER.map((status) => ({ STATUS_ORDER.map((status) => ({
@@ -135,47 +129,69 @@ export function ConfigVersionSwitcher({
return ( return (
<> <>
<div className={cn("flex w-full flex-col gap-2 sm:w-auto sm:min-w-[280px]", className)}> <div className={cn("flex w-full flex-col gap-2 sm:w-auto sm:min-w-[280px]", className)}>
<Label>{label}</Label> <Label className="text-xs text-muted-foreground">{label}</Label>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center"> <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Select <div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
value={selectedId} {selectedVersion ? (
onValueChange={(v) => onSelectedIdChange(v ?? "")} <>
<span className="font-mono text-lg font-semibold leading-none tabular-nums text-foreground">
v{selectedVersion.version_no}
</span>
<ConfigStatusBadge status={selectedVersion.status} />
<span className="text-sm text-muted-foreground">#{selectedVersion.id}</span>
</>
) : (
<span className="text-sm text-muted-foreground">{loading ? "加载中…" : "未选择版本"}</span>
)}
</div>
<Button
type="button"
variant="outline"
disabled={loading || sortedVersions.length === 0} disabled={loading || sortedVersions.length === 0}
onClick={() => setSheetOpen(true)}
> >
<SelectTrigger className="w-full sm:w-[280px]">
<SelectValue placeholder={loading ? "加载中…" : "选择版本"} />
</SelectTrigger>
<SelectContent>
{sortedVersions.map((v) => (
<SelectItem key={v.id} value={String(v.id)}>
{versionSelectLabel(v)}
</SelectItem>
))}
</SelectContent>
</Select>
<Button type="button" variant="outline" disabled={loading} onClick={() => setSheetOpen(true)}>
</Button> </Button>
</div> </div>
</div> </div>
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}> <Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<SheetContent side="right" className="sm:max-w-2xl flex flex-col"> <SheetContent
<SheetHeader> side="right"
<SheetTitle>{sheetTitle}</SheetTitle> className="flex flex-col overflow-hidden border-l-0 bg-[#f6f7fb] p-0 shadow-[-24px_0_80px_rgba(15,23,42,0.22)] sm:max-w-[430px]"
<SheetDescription>{sheetDescription}</SheetDescription> >
</SheetHeader> <div className="relative overflow-hidden border-b border-slate-200/80 bg-white px-5 pb-4 pt-5">
<div className="mt-4 flex gap-2 flex-wrap"> <div className="pointer-events-none absolute -right-10 -top-12 size-32 rounded-full bg-amber-200/45 blur-2xl" />
{statusCounts.map((s) => ( <div className="pointer-events-none absolute right-14 top-7 size-16 rounded-full bg-emerald-200/50 blur-xl" />
<div key={s.status} className="rounded-full border border-border bg-muted/40 px-3 py-1 text-sm text-muted-foreground"> <SheetHeader className="relative space-y-2 text-left">
<span className="font-medium text-foreground">{s.label}</span> <SheetTitle className="text-[17px] font-semibold tracking-tight text-slate-950">
<span className="ml-1 tabular-nums">{s.count}</span> {sheetTitle}
</div> </SheetTitle>
))} <SheetDescription className="max-w-[320px] text-[13px] leading-5 text-slate-500">
{sheetDescription}
</SheetDescription>
</SheetHeader>
</div> </div>
<div className="flex-1 overflow-auto mt-4 space-y-4"> <div className="border-b border-slate-200/80 bg-white/80 px-4 py-3 backdrop-blur">
<div className="grid grid-cols-3 gap-2">
{statusCounts.map((s) => (
<div
key={s.status}
className="rounded-2xl border border-slate-200 bg-white px-3 py-2 shadow-[0_6px_18px_rgba(15,23,42,0.04)]"
>
<p className="text-[11px] font-medium text-slate-500">{s.label}</p>
<p className="mt-0.5 text-lg font-semibold tabular-nums text-slate-950">
{s.count}
</p>
</div>
))}
</div>
</div>
<div className="flex-1 overflow-auto px-4 py-4 space-y-5">
{sortedVersions.length === 0 ? ( {sortedVersions.length === 0 ? (
<Card className="p-4 text-sm text-muted-foreground"></Card> <Card className="border-dashed border-slate-200 bg-white/80 p-5 text-center text-sm text-slate-500 shadow-none">
</Card>
) : ( ) : (
STATUS_ORDER.map((status) => { STATUS_ORDER.map((status) => {
const rows = groupedVersions.get(status) ?? []; const rows = groupedVersions.get(status) ?? [];
@@ -183,72 +199,110 @@ export function ConfigVersionSwitcher({
return null; return null;
} }
return ( return (
<section key={status} className="space-y-2"> <section key={status} className="space-y-2.5">
<div className="flex items-center justify-between px-1"> <div className="flex items-center justify-between px-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2.5">
<ConfigStatusBadge status={status} /> <div
<p className="text-base font-medium text-foreground">{versionStatusLabel(status)}</p> className={cn(
"size-2.5 rounded-full shadow-[0_0_0_4px_rgba(148,163,184,0.12)]",
status === "draft" && "bg-amber-400 shadow-amber-100",
status === "active" && "bg-emerald-500 shadow-emerald-100",
status === "archived" && "bg-slate-400 shadow-slate-100",
)}
/>
<p className="text-[15px] font-semibold text-slate-950">
{versionStatusLabel(status)}
</p>
</div> </div>
<p className="text-sm text-muted-foreground tabular-nums">{rows.length} </p> <p className="rounded-full bg-white px-2 py-0.5 text-xs font-medium tabular-nums text-slate-500 ring-1 ring-slate-200">
{rows.length}
</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2.5">
{rows.map((v) => { {rows.map((v) => {
const isCurrent = selectedId === String(v.id); const isCurrent = selectedId === String(v.id);
return ( return (
<Card <Card
key={v.id} key={v.id}
className={cn( className={cn(
"border-border/70 bg-card/90 p-3 transition-colors", "group overflow-hidden rounded-3xl border border-slate-200/90 bg-white p-0 shadow-[0_12px_34px_rgba(15,23,42,0.06)] transition-all duration-200 hover:-translate-y-0.5 hover:border-slate-300 hover:shadow-[0_18px_42px_rgba(15,23,42,0.1)]",
isCurrent && "border-primary/30 bg-primary/5", isCurrent &&
"border-amber-300 bg-gradient-to-br from-amber-50 via-white to-white shadow-[0_18px_48px_rgba(245,158,11,0.16)] ring-1 ring-amber-200/70",
)} )}
> >
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex gap-3 p-3.5">
<div className="min-w-0 space-y-1"> <div
<div className="flex flex-wrap items-center gap-2"> className={cn(
<span className="font-mono text-base tabular-nums text-foreground">v{v.version_no}</span> "mt-1 h-auto w-1.5 shrink-0 rounded-full bg-slate-200",
<ConfigStatusBadge status={v.status} /> v.status === "draft" && "bg-amber-300",
<span className="text-sm text-muted-foreground">#{v.id}</span> v.status === "active" && "bg-emerald-400",
v.status === "archived" && "bg-slate-300",
)}
/>
<div className="min-w-0 flex-1 space-y-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 space-y-1.5">
<div className="flex flex-wrap items-center gap-2">
<span className="font-mono text-lg font-semibold leading-none tabular-nums text-slate-950">
v{v.version_no}
</span>
<ConfigStatusBadge status={v.status} />
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium tabular-nums text-slate-500">
#{v.id}
</span>
</div>
<p className="line-clamp-2 text-[13px] leading-5 text-slate-500">
{v.effective_at ? formatDt(v.effective_at) : "—"}
{v.reason ? ` · 备注:${v.reason}` : ""}
</p>
</div>
{isCurrent ? (
<span className="shrink-0 rounded-full bg-slate-950 px-2.5 py-1 text-xs font-medium text-white shadow-sm">
</span>
) : null}
</div> </div>
<p className="text-sm text-muted-foreground"> <div className="flex flex-wrap items-center gap-2 border-t border-slate-100 pt-3">
{v.effective_at ? formatDt(v.effective_at) : "—"}
{v.reason ? ` · 备注:${v.reason}` : ""}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button
type="button"
variant={isCurrent ? "secondary" : "outline"}
size="sm"
onClick={() => switchTo(v.id)}
>
{isCurrent ? "当前查看" : "查看"}
</Button>
{onRollbackVersion && v.status !== "draft" ? (
<Button <Button
type="button" type="button"
variant="ghost" variant={isCurrent ? "secondary" : "outline"}
size="sm" size="sm"
disabled={rollbackBusy} className={cn(
onClick={() => { "h-8 rounded-full px-3 text-xs",
onRollbackVersion(v); isCurrent && "bg-slate-100 text-slate-500 hover:bg-slate-100",
setSheetOpen(false); )}
}} onClick={() => switchTo(v.id)}
> >
{isCurrent ? "已选中" : "查看"}
</Button> </Button>
) : null} {onRollbackVersion && v.status !== "draft" ? (
{onDeleteVersion && v.status !== "active" ? ( <Button
<Button type="button"
type="button" variant="ghost"
variant="ghost" size="sm"
size="sm" className="h-8 rounded-full px-3 text-xs text-slate-600 hover:bg-slate-100 hover:text-slate-950"
className="text-destructive" disabled={rollbackBusy}
disabled={deletingId === v.id} onClick={() => {
onClick={() => setDeleteTarget(v)} onRollbackVersion(v);
> setSheetOpen(false);
}}
</Button> >
) : null}
</Button>
) : null}
{onDeleteVersion && v.status !== "active" ? (
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 rounded-full px-3 text-xs text-rose-600 hover:bg-rose-50 hover:text-rose-700"
disabled={deletingId === v.id}
onClick={() => setDeleteTarget(v)}
>
</Button>
) : null}
</div>
</div> </div>
</div> </div>
</Card> </Card>

View File

@@ -315,19 +315,16 @@ export function PlayConfigDocScreen() {
{detail ? ( {detail ? (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
v{detail.version_no} ·{" "}
{detail.status === "active" ? "生效中" : detail.status === "draft" ? "草稿" : "已归档"}
{activeHead ? ( {activeHead ? (
<> <>
{" "} 线 v{activeHead.version_no}
· 线 v{activeHead.version_no}
{activeHead.effective_at ? ` · ${activeHead.effective_at}` : ""} {activeHead.effective_at ? ` · ${activeHead.effective_at}` : ""}
</> </>
) : null} ) : null}
{!isDraft ? ( {!isDraft ? (
<span className="text-amber-600 dark:text-amber-400"> <span className="text-amber-600 dark:text-amber-400">
{" "} {activeHead ? " — " : ""}
稿 稿
</span> </span>
) : null} ) : null}
</p> </p>
@@ -378,10 +375,11 @@ export function PlayConfigDocScreen() {
}} }}
/> />
</TableCell> </TableCell>
<TableCell className="w-[120px]"> <TableCell className="w-[96px]">
<Input <Input
type="number" type="text"
className="h-8 w-full font-mono tabular-nums text-right" inputMode="numeric"
className="h-8 w-16 font-mono tabular-nums text-center"
value={row.display_order} value={row.display_order}
disabled={saving} disabled={saving}
onChange={(e) => { onChange={(e) => {
@@ -394,8 +392,8 @@ export function PlayConfigDocScreen() {
</TableCell> </TableCell>
<TableCell> <TableCell>
<Input <Input
type="number" type="text"
min={0} inputMode="numeric"
className="h-8 font-mono tabular-nums" className="h-8 font-mono tabular-nums"
disabled={!isDraft || saving} disabled={!isDraft || saving}
value={row.min_bet_amount} value={row.min_bet_amount}
@@ -408,8 +406,8 @@ export function PlayConfigDocScreen() {
</TableCell> </TableCell>
<TableCell> <TableCell>
<Input <Input
type="number" type="text"
min={0} inputMode="numeric"
className="h-8 font-mono tabular-nums" className="h-8 font-mono tabular-nums"
disabled={!isDraft || saving} disabled={!isDraft || saving}
value={row.max_bet_amount} value={row.max_bet_amount}