feat: 重构开奖模块导航与面包屑,优化期号详情与列表展示
This commit is contained in:
@@ -4,7 +4,7 @@ import { WalletTxnsPanel } from "@/modules/wallet/wallet-console";
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: `${walletModuleMeta.title} · 流水`,
|
title: `${walletModuleMeta.title} · 钱包流水`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AdminWalletTransactionsPage() {
|
export default function AdminWalletTransactionsPage() {
|
||||||
|
|||||||
96
src/components/admin/admin-breadcrumb.tsx
Normal file
96
src/components/admin/admin-breadcrumb.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { adminShellNavItems, ADMIN_BASE } from "@/modules/_config/admin-nav";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const DRAW_ROUTE_LABELS: Record<string, string> = {
|
||||||
|
finance: "期号收支",
|
||||||
|
review: "审核",
|
||||||
|
results: "开奖结果",
|
||||||
|
};
|
||||||
|
|
||||||
|
function titleCase(value: string): string {
|
||||||
|
return value
|
||||||
|
.split("-")
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdminBreadcrumb() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
// 把路径拆分成段
|
||||||
|
const segments = pathname.split("/").filter(Boolean);
|
||||||
|
|
||||||
|
// 基础面包屑:首页/仪表盘
|
||||||
|
const breadcrumbs = [
|
||||||
|
{
|
||||||
|
label: "首页",
|
||||||
|
href: ADMIN_BASE,
|
||||||
|
isCurrent: pathname === ADMIN_BASE,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (pathname !== ADMIN_BASE) {
|
||||||
|
const businessSegment = segments[1];
|
||||||
|
if (businessSegment) {
|
||||||
|
const navItem = adminShellNavItems.find((item) => {
|
||||||
|
return item.segment === businessSegment || item.href.includes(businessSegment);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (navItem && navItem.href !== ADMIN_BASE) {
|
||||||
|
breadcrumbs.push({
|
||||||
|
label: navItem.segment === "draws" ? "期号列表" : navItem.label,
|
||||||
|
href: navItem.href,
|
||||||
|
isCurrent: pathname === navItem.href || segments.length === 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segments.length > 2) {
|
||||||
|
const subSegment = segments[2];
|
||||||
|
const subLabel = subSegment ? DRAW_ROUTE_LABELS[subSegment] ?? titleCase(subSegment) : "";
|
||||||
|
if (subLabel) {
|
||||||
|
breadcrumbs.push({
|
||||||
|
label: subLabel,
|
||||||
|
href: pathname,
|
||||||
|
isCurrent: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
{breadcrumbs.map((crumb, index) => {
|
||||||
|
const isLast = index === breadcrumbs.length - 1;
|
||||||
|
const itemKey = `${crumb.href}-${index}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={itemKey}>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
{crumb.isCurrent ? (
|
||||||
|
<BreadcrumbPage>{crumb.label}</BreadcrumbPage>
|
||||||
|
) : (
|
||||||
|
<BreadcrumbLink render={<Link href={crumb.href} />}>{crumb.label}</BreadcrumbLink>
|
||||||
|
)}
|
||||||
|
</BreadcrumbItem>
|
||||||
|
{!isLast && <BreadcrumbSeparator />}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,11 +4,13 @@ import type { ReactNode } from "react";
|
|||||||
|
|
||||||
import { AdminAppSidebar } from "@/components/admin/admin-sidebar";
|
import { AdminAppSidebar } from "@/components/admin/admin-sidebar";
|
||||||
import { ShellToolbar } from "@/components/admin/toolbar";
|
import { ShellToolbar } from "@/components/admin/toolbar";
|
||||||
|
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
|
||||||
import {
|
import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
export function AdminShell({ children }: { children: ReactNode }) {
|
export function AdminShell({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@@ -17,9 +19,8 @@ export function AdminShell({ children }: { children: ReactNode }) {
|
|||||||
<SidebarInset className="max-md:overflow-x-hidden">
|
<SidebarInset className="max-md:overflow-x-hidden">
|
||||||
<header className="sticky top-0 z-30 flex min-h-12 items-center gap-3 border-b border-border bg-background/80 px-4 py-2 backdrop-blur-md">
|
<header className="sticky top-0 z-30 flex min-h-12 items-center gap-3 border-b border-border bg-background/80 px-4 py-2 backdrop-blur-md">
|
||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
<span className="text-sm font-medium text-muted-foreground md:hidden">
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
彩票后台
|
<AdminBreadcrumb />
|
||||||
</span>
|
|
||||||
<div className="ml-auto flex shrink-0 items-center">
|
<div className="ml-auto flex shrink-0 items-center">
|
||||||
<ShellToolbar />
|
<ShellToolbar />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
125
src/components/ui/breadcrumb.tsx
Normal file
125
src/components/ui/breadcrumb.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { mergeProps } from "@base-ui/react/merge-props"
|
||||||
|
import { useRender } from "@base-ui/react/use-render"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
aria-label="breadcrumb"
|
||||||
|
data-slot="breadcrumb"
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
|
return (
|
||||||
|
<ol
|
||||||
|
data-slot="breadcrumb-list"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="breadcrumb-item"
|
||||||
|
className={cn("inline-flex items-center gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbLink({
|
||||||
|
className,
|
||||||
|
render,
|
||||||
|
...props
|
||||||
|
}: useRender.ComponentProps<"a">) {
|
||||||
|
return useRender({
|
||||||
|
defaultTagName: "a",
|
||||||
|
props: mergeProps<"a">(
|
||||||
|
{
|
||||||
|
className: cn("transition-colors hover:text-foreground", className),
|
||||||
|
},
|
||||||
|
props
|
||||||
|
),
|
||||||
|
render,
|
||||||
|
state: {
|
||||||
|
slot: "breadcrumb-link",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="breadcrumb-page"
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
className={cn("font-normal text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbSeparator({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="breadcrumb-separator"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? (
|
||||||
|
<ChevronRightIcon />
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="breadcrumb-ellipsis"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"flex size-5 items-center justify-center [&>svg]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon
|
||||||
|
/>
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
BreadcrumbEllipsis,
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@ export const adminShellNavItems: AdminNavItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
segment: "draws",
|
segment: "draws",
|
||||||
label: "开奖",
|
label: "期号列表",
|
||||||
href: "/admin/draws",
|
href: "/admin/draws",
|
||||||
requiredAny: ["prd.draw_result.manage", "prd.draw_result.view"],
|
requiredAny: ["prd.draw_result.manage", "prd.draw_result.view"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -67,55 +67,61 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card>
|
<Card className="overflow-hidden">
|
||||||
<CardHeader>
|
<CardHeader className="border-b bg-muted/30 pb-4">
|
||||||
<CardTitle className="text-lg">当期状态</CardTitle>
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
</CardHeader>
|
<div>
|
||||||
<CardContent className="space-y-4">
|
<CardTitle className="text-xl">{data.draw_no}</CardTitle>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<p className="mt-1 text-sm text-muted-foreground">开奖详情</p>
|
||||||
<span className="font-mono text-base font-semibold">{data.draw_no}</span>
|
|
||||||
<DrawStatusBadge status={data.status} label={`DB · ${data.status}`} />
|
|
||||||
<DrawStatusBadge
|
|
||||||
status={data.hall_preview_status}
|
|
||||||
label={`大厅预览 · ${data.hall_preview_status}`}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<div className="grid gap-4">
|
<DrawStatusBadge status={data.status} label={data.status} />
|
||||||
|
<DrawStatusBadge status={data.hall_preview_status} label={`大厅预览 ${data.hall_preview_status}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-6 p-6 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<Field label="业务日">{data.business_date}</Field>
|
<Field label="业务日">{data.business_date}</Field>
|
||||||
<Field label="流水序号">{data.sequence_no}</Field>
|
<Field label="流水序号">{data.sequence_no}</Field>
|
||||||
<Field label="开始时间">{formatDt(data.start_time)}</Field>
|
<Field label="开始时间">{formatDt(data.start_time)}</Field>
|
||||||
<Field label="封盘时间">{formatDt(data.close_time)}</Field>
|
<Field label="封盘时间">{formatDt(data.close_time)}</Field>
|
||||||
<Field label="计划开奖">{formatDt(data.draw_time)}</Field>
|
<Field label="计划开奖">{formatDt(data.draw_time)}</Field>
|
||||||
<Field label="冷静期结束">{formatDt(data.cooling_end_time)}</Field>
|
<Field label="冷静期结束">{formatDt(data.cooling_end_time)}</Field>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<Field label="结果来源">{data.result_source ?? "—"}</Field>
|
<Field label="结果来源">{data.result_source ?? "—"}</Field>
|
||||||
<Field label="当前结果版本">{data.current_result_version}</Field>
|
<Field label="当前结果版本">{data.current_result_version}</Field>
|
||||||
<Field label="结算版本">{data.settle_version}</Field>
|
<Field label="结算版本">{data.settle_version}</Field>
|
||||||
<Field label="是否重开">{data.is_reopened ? "是" : "否"}</Field>
|
<Field label="是否重开">{data.is_reopened ? "是" : "否"}</Field>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<div className="rounded-xl border bg-muted/20 p-4">
|
||||||
<CardHeader>
|
<p className="text-sm font-medium text-muted-foreground">批次统计</p>
|
||||||
<CardTitle className="text-base">批次统计</CardTitle>
|
<div className="mt-3 grid gap-3 text-sm">
|
||||||
</CardHeader>
|
<div className="flex items-center justify-between rounded-lg bg-background px-3 py-2">
|
||||||
<CardContent className="flex flex-col gap-4">
|
<span>总批次</span>
|
||||||
<div className="flex flex-wrap gap-6 text-sm">
|
<span className="font-semibold">{data.result_batch_counts.total}</span>
|
||||||
<span>总批次:{data.result_batch_counts.total}</span>
|
</div>
|
||||||
<span className="text-amber-600 dark:text-amber-400">
|
<div className="flex items-center justify-between rounded-lg bg-background px-3 py-2 text-amber-600 dark:text-amber-400">
|
||||||
待审核:{data.result_batch_counts.pending_review}
|
<span>待审核</span>
|
||||||
</span>
|
<span className="font-semibold">{data.result_batch_counts.pending_review}</span>
|
||||||
<span className="text-emerald-600 dark:text-emerald-400">
|
</div>
|
||||||
已发布:{data.result_batch_counts.published}
|
<div className="flex items-center justify-between rounded-lg bg-background px-3 py-2 text-emerald-600 dark:text-emerald-400">
|
||||||
</span>
|
<span>已发布</span>
|
||||||
|
<span className="font-semibold">{data.result_batch_counts.published}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/draws/${drawId}/finance`}
|
href={`/admin/draws/${drawId}/finance`}
|
||||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "w-fit")}
|
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "mt-4 w-full")}
|
||||||
>
|
>
|
||||||
期号收支(客服/财务)
|
查看期号收支
|
||||||
</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -117,28 +117,22 @@ export function DrawsIndexConsole() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* Grid:桌面端标签一行 / 控件一行,避免 flex+items-end 与各列实际高度不一致;移动端单列自上而下 */}
|
{/* Grid:桌面端标签一行 / 控件一行,避免 flex+items-end 与各列实际高度不一致;移动端单列自上而下 */}
|
||||||
<div
|
<div
|
||||||
className="
|
className="grid max-w-full gap-x-6 gap-y-3 sm:grid-cols-[minmax(0,12rem)_minmax(0,11rem)_auto] sm:gap-y-1.5"
|
||||||
grid max-w-full gap-x-6 gap-y-3
|
|
||||||
[grid-template-areas:'dl'_'di'_'sl'_'si'_'act']
|
|
||||||
sm:grid-cols-[minmax(0,12rem)_minmax(0,11rem)_auto]
|
|
||||||
sm:gap-y-1.5
|
|
||||||
sm:[grid-template-areas:'dl_sl_ah'_'di_si_act']
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<Label htmlFor="draw-filter-no" className="[grid-area:dl]">
|
<Label htmlFor="draw-filter-no">
|
||||||
期号
|
期号
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="draw-filter-no"
|
id="draw-filter-no"
|
||||||
placeholder="模糊匹配期号"
|
placeholder="模糊匹配期号"
|
||||||
value={draftDrawNo}
|
value={draftDrawNo}
|
||||||
className="[grid-area:di] w-full min-w-0 sm:w-full"
|
className="w-full min-w-0 sm:w-full"
|
||||||
onChange={(e) => setDraftDrawNo(e.target.value)}
|
onChange={(e) => setDraftDrawNo(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="draw-filter-status" className="[grid-area:sl]">
|
<Label htmlFor="draw-filter-status">
|
||||||
状态
|
状态
|
||||||
</Label>
|
</Label>
|
||||||
<div className="[grid-area:si] min-w-0">
|
<div className="min-w-0">
|
||||||
<Select
|
<Select
|
||||||
modal={false}
|
modal={false}
|
||||||
value={
|
value={
|
||||||
@@ -164,13 +158,6 @@ export function DrawsIndexConsole() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<span
|
|
||||||
className="[grid-area:ah] hidden select-none text-sm font-medium leading-none sm:block"
|
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
{/* 占位:与 Label 行同高,使按钮与输入控件同行对齐 */}
|
|
||||||
|
|
||||||
</span>
|
|
||||||
<div className="[grid-area:act] flex flex-wrap gap-2">
|
<div className="[grid-area:act] flex flex-wrap gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -180,7 +167,7 @@ export function DrawsIndexConsole() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
查询
|
查询期号
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -193,7 +180,7 @@ export function DrawsIndexConsole() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
重置筛选
|
重置
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -240,7 +227,7 @@ export function DrawsIndexConsole() {
|
|||||||
href={`/admin/draws/${row.id}`}
|
href={`/admin/draws/${row.id}`}
|
||||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
|
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
|
||||||
>
|
>
|
||||||
详情
|
查看详情
|
||||||
</Link>
|
</Link>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const drawsModuleMeta = {
|
export const drawsModuleMeta = {
|
||||||
segment: "draws",
|
segment: "draws",
|
||||||
title: "开奖",
|
title: "期号列表",
|
||||||
description: "",
|
description: "",
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user