refactor(config): 优化版本切换抽屉与玩法配置输入展示
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user