feat(dashboard, i18n): 增强仪表盘视觉效果与多语言支持

在英文、尼泊尔语和中文语言包中新增 “Other statuses” 翻译,提升仪表盘指标展示的清晰度。
在仪表盘中集成新的 StatCard 组件,用于更直观地展示关键指标数据。
更新仪表盘趋势图表,采用 recharts 实现更丰富的数据可视化效果。
重构现有组件与布局,优化整体交互与用户体验,使界面更加直观易用。
This commit is contained in:
2026-05-26 16:32:11 +08:00
parent eb83bcf360
commit 0bd9d8d3d8
11 changed files with 1580 additions and 515 deletions

View File

@@ -34,6 +34,7 @@ import {
HotUsageBars,
PayoutCompositionChart,
ResultBatchProgress,
StatCard,
SettlementStatusChart,
SoldOutRing,
} from "@/modules/dashboard/dashboard-visuals";
@@ -302,51 +303,45 @@ export function DashboardConsole(): ReactElement {
</div>
) : null}
<DashboardAnalyticsPanel enabled={canFinance} playOptions={playOptions} />
<div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-4">
<StatCard
label={t("pendingReviewResults")}
value={pendingReview ?? "—"}
hint={t("resultBatches")}
icon={<ClipboardList className="size-5" aria-hidden />}
accent={(pendingReview ?? 0) > 0 ? "destructive" : "muted"}
/>
<StatCard
label={t("abnormalTransferOrders")}
value={abnormalTransferTotal ?? "—"}
hint={t("viewTransferOrders")}
icon={<AlertTriangle className="size-5" aria-hidden />}
accent={(abnormalTransferTotal ?? 0) > 0 ? "destructive" : "muted"}
/>
<StatCard
label={t("riskCapUsage")}
value={`${usagePct.toFixed(1)}%`}
hint={t("lockedAndCap", { locked: formatMoneyMinor(riskLocked, currency), cap: formatMoneyMinor(riskCap, currency) })}
icon={<Shield className="size-5" aria-hidden />}
accent={usagePct >= 90 ? "destructive" : usagePct >= 70 ? "primary" : "muted"}
/>
<StatCard
label={t("sections.currentDraw")}
value={hall?.draw_no ?? "—"}
hint={t("drawSequence", { sequence: hall?.sequence_no ?? "—" })}
icon={<Ticket className="size-5" aria-hidden />}
accent="primary"
/>
</div>
<h2 className="text-sm font-semibold tracking-wide text-muted-foreground">{t("sections.operations")}</h2>
<div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
<Card className="border-border/80 shadow-sm xl:col-span-1">
<CardHeader className="pb-2">
<CardTitle className="text-base">{t("financeStructure")}</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<Skeleton className="h-36 w-full" />
) : finance ? (
<FinanceStructureChart finance={finance} formatMoney={formatMoneyMinor} />
) : (
<p className="py-8 text-center text-sm text-muted-foreground">{t("states.noData", { ns: "common" })}</p>
)}
</CardContent>
</Card>
<Card className="border-border/80 shadow-sm xl:col-span-1">
<CardHeader className="pb-2">
<CardTitle className="text-base">{t("payoutComposition")}</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<Skeleton className="h-36 w-full" />
) : finance ? (
<PayoutCompositionChart finance={finance} formatMoney={formatMoneyMinor} />
) : (
<p className="py-8 text-center text-sm text-muted-foreground">{t("states.noData", { ns: "common" })}</p>
)}
</CardContent>
</Card>
<Card className="border-border/80 shadow-sm lg:col-span-2 xl:col-span-1">
<div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-4">
<Card className="border-border/80 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base">{t("riskCapUsage")}</CardTitle>
{drawId != null ? (
<Link
href={`/admin/draws/${drawId}/risk/occupancy`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"h-7 px-2 text-xs",
)}
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-7 px-2 text-xs")}
>
{t("occupancyDetails")}
</Link>
@@ -354,7 +349,7 @@ export function DashboardConsole(): ReactElement {
</CardHeader>
<CardContent>
{loading ? (
<Skeleton className="h-24 w-full" />
<Skeleton className="h-44 w-full" />
) : (
<CapUsageBar
locked={riskLocked}
@@ -366,51 +361,6 @@ export function DashboardConsole(): ReactElement {
)}
</CardContent>
</Card>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<Card className="border-border/80 shadow-sm">
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-2 space-y-0 pb-2">
<CardTitle className="text-base">{t("hotNumbersTop10")}</CardTitle>
<div className="flex items-center gap-2">
<div role="tablist" aria-label={t("playDimension")} className="flex gap-1">
{([
{ value: "4D", label: t("tabs.4d") },
{ value: "3D", label: t("tabs.3d") },
{ value: "2D", label: t("tabs.2d") },
{ value: "special", label: t("tabs.special") },
] as const).map((tab) => (
<button
key={tab.value}
type="button"
role="tab"
aria-selected={hotTab === tab.value}
className={cn(
"rounded-md px-2.5 py-1 text-xs font-medium transition-colors",
hotTab === tab.value
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted",
)}
onClick={() => setHotTab(tab.value)}
>
{tab.label}
</button>
))}
</div>
{drawId != null ? (
<Link
href={`/admin/draws/${drawId}/risk/hot`}
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-7 px-2 text-xs")}
>
{t("actions.viewAll", { ns: "common" })}
</Link>
) : null}
</div>
</CardHeader>
<CardContent>
{loading ? <Skeleton className="h-64 w-full" /> : <HotUsageBars rows={hotRows} />}
</CardContent>
</Card>
<Card className="border-border/80 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
@@ -426,7 +376,7 @@ export function DashboardConsole(): ReactElement {
</CardHeader>
<CardContent>
{loading ? (
<Skeleton className="h-64 w-full" />
<Skeleton className="h-44 w-full" />
) : soldOutBuckets ? (
<SoldOutRing buckets={soldOutBuckets} />
) : (
@@ -434,9 +384,7 @@ export function DashboardConsole(): ReactElement {
)}
</CardContent>
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2">
<Card className="border-border/80 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base">{t("resultBatches")}</CardTitle>
@@ -451,7 +399,7 @@ export function DashboardConsole(): ReactElement {
</CardHeader>
<CardContent>
{loading ? (
<Skeleton className="h-28 w-full" />
<Skeleton className="h-44 w-full" />
) : drawPanel ? (
<ResultBatchProgress draw={drawPanel} />
) : (
@@ -461,6 +409,40 @@ export function DashboardConsole(): ReactElement {
</Card>
<Card className="border-border/80 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base">{t("payoutComposition")}</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<Skeleton className="h-44 w-full" />
) : finance ? (
<PayoutCompositionChart finance={finance} formatMoney={formatMoneyMinor} />
) : (
<p className="py-8 text-center text-sm text-muted-foreground">{t("states.noData", { ns: "common" })}</p>
)}
</CardContent>
</Card>
</div>
<DashboardAnalyticsPanel enabled={canFinance} playOptions={playOptions} />
<div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
<Card className="border-border/80 shadow-sm xl:col-span-1">
<CardHeader className="pb-2">
<CardTitle className="text-base">{t("financeStructure")}</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<Skeleton className="h-40 w-full" />
) : finance ? (
<FinanceStructureChart finance={finance} formatMoney={formatMoneyMinor} />
) : (
<p className="py-8 text-center text-sm text-muted-foreground">{t("states.noData", { ns: "common" })}</p>
)}
</CardContent>
</Card>
<Card className="border-border/80 shadow-sm xl:col-span-1">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base">{t("settlementOverview")}</CardTitle>
{drawId != null ? (
@@ -474,7 +456,7 @@ export function DashboardConsole(): ReactElement {
</CardHeader>
<CardContent>
{loading ? (
<Skeleton className="h-28 w-full" />
<Skeleton className="h-40 w-full" />
) : finance ? (
<SettlementStatusChart finance={finance} />
) : (
@@ -482,8 +464,49 @@ export function DashboardConsole(): ReactElement {
)}
</CardContent>
</Card>
<Card className="border-border/80 shadow-sm xl:col-span-1">
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-2 space-y-0 pb-2">
<CardTitle className="text-base">{t("hotNumbersTop10")}</CardTitle>
<div className="flex items-center gap-2">
<div role="tablist" aria-label={t("playDimension")} className="flex gap-1">
{([
{ value: "4D", label: t("tabs.4d") },
{ value: "3D", label: t("tabs.3d") },
{ value: "2D", label: t("tabs.2d") },
{ value: "special", label: t("tabs.special") },
] as const).map((tab) => (
<button
key={tab.value}
type="button"
role="tab"
aria-selected={hotTab === tab.value}
className={cn(
"rounded-md px-2.5 py-1 text-xs font-medium transition-colors",
hotTab === tab.value ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted",
)}
onClick={() => setHotTab(tab.value)}
>
{tab.label}
</button>
))}
</div>
{drawId != null ? (
<Link
href={`/admin/draws/${drawId}/risk/hot`}
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-7 px-2 text-xs")}
>
{t("actions.viewAll", { ns: "common" })}
</Link>
) : null}
</div>
</CardHeader>
<CardContent>{loading ? <Skeleton className="h-64 w-full" /> : <HotUsageBars rows={hotRows} />}</CardContent>
</Card>
</div>
<h2 className="text-sm font-semibold tracking-wide text-muted-foreground">{t("sections.operations")}</h2>
<div className="grid gap-4 md:grid-cols-2">
<div className="flex flex-col justify-between gap-4 rounded-xl border border-border/80 bg-card p-5 shadow-sm sm:flex-row sm:items-center">
<div className="flex gap-4">
@@ -523,25 +546,27 @@ export function DashboardConsole(): ReactElement {
</div>
</div>
<Card className="border-border/80 shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base">{t("quickLinksTitle")}</CardTitle>
</CardHeader>
<CardContent className="flex flex-wrap justify-center gap-3 py-2 sm:gap-5">
{quickLinks.map((q) => (
<Link
key={q.label}
href={q.href}
className="flex w-24 flex-col items-center gap-2 rounded-lg border border-transparent p-2 text-center text-xs font-medium text-foreground transition-colors hover:border-border hover:bg-muted/50"
>
<span className="flex size-11 items-center justify-center rounded-full border border-border bg-card text-foreground shadow-sm">
{q.icon}
</span>
{q.label}
</Link>
))}
</CardContent>
</Card>
<div className="grid gap-4">
<Card className="border-border/80 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base">{t("quickLinksTitle")}</CardTitle>
</CardHeader>
<CardContent className="flex flex-wrap justify-center gap-3 py-2 sm:gap-5">
{quickLinks.map((q) => (
<Link
key={q.label}
href={q.href}
className="flex w-24 flex-col items-center gap-2 rounded-lg border border-transparent p-2 text-center text-xs font-medium text-foreground transition-colors hover:border-border hover:bg-muted/50"
>
<span className="flex size-11 items-center justify-center rounded-full border border-border bg-card text-foreground shadow-sm">
{q.icon}
</span>
{q.label}
</Link>
))}
</CardContent>
</Card>
</div>
</div>
);
}