feat(dashboard, i18n): 增强仪表盘视觉效果与多语言支持
在英文、尼泊尔语和中文语言包中新增 “Other statuses” 翻译,提升仪表盘指标展示的清晰度。 在仪表盘中集成新的 StatCard 组件,用于更直观地展示关键指标数据。 更新仪表盘趋势图表,采用 recharts 实现更丰富的数据可视化效果。 重构现有组件与布局,优化整体交互与用户体验,使界面更加直观易用。
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user