refactor(config): 优化版本切换抽屉与玩法配置输入展示
This commit is contained in:
@@ -13,13 +13,6 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -45,10 +38,6 @@ function versionStatusLabel(status: string): string {
|
||||
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;
|
||||
|
||||
export type ConfigVersionSwitcherProps = {
|
||||
@@ -101,6 +90,11 @@ export function ConfigVersionSwitcher({
|
||||
return groups;
|
||||
}, [sortedVersions]);
|
||||
|
||||
const selectedVersion = useMemo(
|
||||
() => sortedVersions.find((v) => String(v.id) === selectedId) ?? null,
|
||||
[selectedId, sortedVersions],
|
||||
);
|
||||
|
||||
const statusCounts = useMemo(
|
||||
() =>
|
||||
STATUS_ORDER.map((status) => ({
|
||||
@@ -135,47 +129,69 @@ export function ConfigVersionSwitcher({
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<Select
|
||||
value={selectedId}
|
||||
onValueChange={(v) => onSelectedIdChange(v ?? "")}
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
||||
{selectedVersion ? (
|
||||
<>
|
||||
<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}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||
<SheetContent side="right" className="sm:max-w-2xl flex flex-col">
|
||||
<SheetHeader>
|
||||
<SheetTitle>{sheetTitle}</SheetTitle>
|
||||
<SheetDescription>{sheetDescription}</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="mt-4 flex gap-2 flex-wrap">
|
||||
{statusCounts.map((s) => (
|
||||
<div key={s.status} className="rounded-full border border-border bg-muted/40 px-3 py-1 text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">{s.label}</span>
|
||||
<span className="ml-1 tabular-nums">{s.count}</span>
|
||||
</div>
|
||||
))}
|
||||
<SheetContent
|
||||
side="right"
|
||||
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]"
|
||||
>
|
||||
<div className="relative overflow-hidden border-b border-slate-200/80 bg-white px-5 pb-4 pt-5">
|
||||
<div className="pointer-events-none absolute -right-10 -top-12 size-32 rounded-full bg-amber-200/45 blur-2xl" />
|
||||
<div className="pointer-events-none absolute right-14 top-7 size-16 rounded-full bg-emerald-200/50 blur-xl" />
|
||||
<SheetHeader className="relative space-y-2 text-left">
|
||||
<SheetTitle className="text-[17px] font-semibold tracking-tight text-slate-950">
|
||||
{sheetTitle}
|
||||
</SheetTitle>
|
||||
<SheetDescription className="max-w-[320px] text-[13px] leading-5 text-slate-500">
|
||||
{sheetDescription}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
</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 ? (
|
||||
<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) => {
|
||||
const rows = groupedVersions.get(status) ?? [];
|
||||
@@ -183,72 +199,110 @@ export function ConfigVersionSwitcher({
|
||||
return null;
|
||||
}
|
||||
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 gap-2">
|
||||
<ConfigStatusBadge status={status} />
|
||||
<p className="text-base font-medium text-foreground">{versionStatusLabel(status)}</p>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div
|
||||
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>
|
||||
<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 className="space-y-2">
|
||||
<div className="space-y-2.5">
|
||||
{rows.map((v) => {
|
||||
const isCurrent = selectedId === String(v.id);
|
||||
return (
|
||||
<Card
|
||||
key={v.id}
|
||||
className={cn(
|
||||
"border-border/70 bg-card/90 p-3 transition-colors",
|
||||
isCurrent && "border-primary/30 bg-primary/5",
|
||||
"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-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="min-w-0 space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-mono text-base tabular-nums text-foreground">v{v.version_no}</span>
|
||||
<ConfigStatusBadge status={v.status} />
|
||||
<span className="text-sm text-muted-foreground">#{v.id}</span>
|
||||
<div className="flex gap-3 p-3.5">
|
||||
<div
|
||||
className={cn(
|
||||
"mt-1 h-auto w-1.5 shrink-0 rounded-full bg-slate-200",
|
||||
v.status === "draft" && "bg-amber-300",
|
||||
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>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
生效时间:{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" ? (
|
||||
<div className="flex flex-wrap items-center gap-2 border-t border-slate-100 pt-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant={isCurrent ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
disabled={rollbackBusy}
|
||||
onClick={() => {
|
||||
onRollbackVersion(v);
|
||||
setSheetOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"h-8 rounded-full px-3 text-xs",
|
||||
isCurrent && "bg-slate-100 text-slate-500 hover:bg-slate-100",
|
||||
)}
|
||||
onClick={() => switchTo(v.id)}
|
||||
>
|
||||
回滚
|
||||
{isCurrent ? "已选中" : "查看"}
|
||||
</Button>
|
||||
) : null}
|
||||
{onDeleteVersion && v.status !== "active" ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive"
|
||||
disabled={deletingId === v.id}
|
||||
onClick={() => setDeleteTarget(v)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
) : null}
|
||||
{onRollbackVersion && v.status !== "draft" ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 rounded-full px-3 text-xs text-slate-600 hover:bg-slate-100 hover:text-slate-950"
|
||||
disabled={rollbackBusy}
|
||||
onClick={() => {
|
||||
onRollbackVersion(v);
|
||||
setSheetOpen(false);
|
||||
}}
|
||||
>
|
||||
回滚
|
||||
</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>
|
||||
</Card>
|
||||
|
||||
@@ -315,19 +315,16 @@ export function PlayConfigDocScreen() {
|
||||
|
||||
{detail ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
当前版本:v{detail.version_no} ·{" "}
|
||||
{detail.status === "active" ? "生效中" : detail.status === "draft" ? "草稿" : "已归档"}
|
||||
{activeHead ? (
|
||||
<>
|
||||
{" "}
|
||||
· 线上生效版本 v{activeHead.version_no}
|
||||
线上生效版本 v{activeHead.version_no}
|
||||
{activeHead.effective_at ? ` · ${activeHead.effective_at}` : ""}
|
||||
</>
|
||||
) : null}
|
||||
{!isDraft ? (
|
||||
<span className="text-amber-600 dark:text-amber-400">
|
||||
{" "}
|
||||
— 限额与规则为只读,请先新建草稿。
|
||||
{activeHead ? " — " : ""}
|
||||
限额与规则为只读,请先新建草稿。
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
@@ -378,10 +375,11 @@ export function PlayConfigDocScreen() {
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="w-[120px]">
|
||||
<TableCell className="w-[96px]">
|
||||
<Input
|
||||
type="number"
|
||||
className="h-8 w-full font-mono tabular-nums text-right"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
className="h-8 w-16 font-mono tabular-nums text-center"
|
||||
value={row.display_order}
|
||||
disabled={saving}
|
||||
onChange={(e) => {
|
||||
@@ -394,8 +392,8 @@ export function PlayConfigDocScreen() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
className="h-8 font-mono tabular-nums"
|
||||
disabled={!isDraft || saving}
|
||||
value={row.min_bet_amount}
|
||||
@@ -408,8 +406,8 @@ export function PlayConfigDocScreen() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
className="h-8 font-mono tabular-nums"
|
||||
disabled={!isDraft || saving}
|
||||
value={row.max_bet_amount}
|
||||
|
||||
Reference in New Issue
Block a user