feat(admin): 仪表盘重构、代理线路修复与后台体验优化
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
- 各角色仪表盘:KPI 可点击跳转、快捷入口、去重冗余区块,代理/站点降级 analytics - 代理线路:创建玩家权限、侧栏搜索、深链 URL、档案保存与删除预检等修复 - 统一密码最短 6 位;代理模块表格/侧栏背景色一致(admin-table-inset) - 报表、钱包、对账、开奖、结算、配置等模块结构与 i18n 同步更新
This commit is contained in:
@@ -4,7 +4,13 @@ import Link from "next/link";
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AlertTriangle, ArrowRightIcon, CheckCircle2, ChevronRightIcon } from "lucide-react";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowRightIcon,
|
||||
CheckCircle2,
|
||||
ChevronRightIcon,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
@@ -19,7 +25,7 @@ import {
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminMoneyDisplay } from "@/components/admin/admin-money-display";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -268,6 +274,7 @@ export function DashboardKpiCard({
|
||||
currencyCode,
|
||||
sparklineValues,
|
||||
deltaLabel,
|
||||
href,
|
||||
}: {
|
||||
label: string;
|
||||
value?: ReactNode;
|
||||
@@ -281,6 +288,8 @@ export function DashboardKpiCard({
|
||||
currencyCode?: string | null;
|
||||
sparklineValues?: number[];
|
||||
deltaLabel?: ReactNode;
|
||||
/** 整张卡片可点击跳转 */
|
||||
href?: string;
|
||||
}): ReactElement {
|
||||
const resolvedValue =
|
||||
typeof signedAmountMinor === "number" && currencyCode !== undefined
|
||||
@@ -295,17 +304,30 @@ export function DashboardKpiCard({
|
||||
? String(resolvedValue)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-w-0 flex-col rounded-xl border border-border/60 bg-card p-4">
|
||||
const card = (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full min-w-0 flex-col rounded-xl border border-border/60 bg-card p-4",
|
||||
href && "transition-colors group-hover/kpi:border-primary/30 group-hover/kpi:shadow-sm",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="min-w-0 flex-1 text-xs font-medium leading-snug text-muted-foreground">{label}</p>
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-9 shrink-0 items-center justify-center rounded-lg [&_svg]:size-4",
|
||||
kpiAccentClass(accent),
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-9 items-center justify-center rounded-lg [&_svg]:size-4",
|
||||
kpiAccentClass(accent),
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
{href ? (
|
||||
<ChevronRightIcon
|
||||
className="size-4 text-muted-foreground/50 transition group-hover/kpi:text-primary"
|
||||
aria-hidden
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
@@ -336,10 +358,126 @@ export function DashboardKpiCard({
|
||||
</div>
|
||||
) : null}
|
||||
{hint ? (
|
||||
<p className="mt-2 text-[11px] leading-snug text-muted-foreground">{hint}</p>
|
||||
<p
|
||||
className={cn(
|
||||
"mt-2 text-[11px] leading-snug",
|
||||
href ? "text-primary/80 group-hover/kpi:underline" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{hint}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!href) {
|
||||
return card;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="group/kpi block h-full min-h-0 rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{card}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export type DashboardQuickLink = {
|
||||
href: string;
|
||||
label: string;
|
||||
icon?: LucideIcon;
|
||||
};
|
||||
|
||||
/** 仪表盘快捷入口:按钮或图标卡片两种布局 */
|
||||
export function DashboardQuickLinksCard({
|
||||
title,
|
||||
links,
|
||||
variant = "buttons",
|
||||
}: {
|
||||
title: string;
|
||||
links: readonly DashboardQuickLink[];
|
||||
variant?: "buttons" | "tiles";
|
||||
}): ReactElement {
|
||||
const { t } = useTranslation("dashboard");
|
||||
|
||||
if (links.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent
|
||||
className={cn(
|
||||
variant === "buttons" ? "flex flex-wrap gap-2" : "grid gap-3 sm:grid-cols-3",
|
||||
)}
|
||||
>
|
||||
{links.map((link) => {
|
||||
if (variant === "buttons") {
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-3 py-1.5 text-sm font-medium shadow-xs transition-colors hover:bg-accent hover:text-accent-foreground",
|
||||
)}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
const Icon = link.icon;
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="flex flex-col gap-2 rounded-xl border bg-muted/20 px-4 py-4 transition-colors hover:bg-muted/40"
|
||||
>
|
||||
{Icon ? <Icon className="size-5 text-primary" aria-hidden /> : null}
|
||||
<span className="text-sm font-medium">{link.label}</span>
|
||||
<span className="text-xs text-muted-foreground">{t("cs.openModule")}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/** 轻量报表入口:替代站点/代理仪表盘内嵌完整 analytics 面板 */
|
||||
export function DashboardReportsTeaser({
|
||||
title,
|
||||
description,
|
||||
href = "/admin/reports",
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
href?: string;
|
||||
}): ReactElement {
|
||||
const { t } = useTranslation("dashboard");
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2 space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{title}</CardTitle>
|
||||
<Link
|
||||
href={href}
|
||||
className="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
|
||||
>
|
||||
{t("viewReports")}
|
||||
<ArrowRightIcon className="size-3.5" aria-hidden />
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">{description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function MiniSparkline({
|
||||
|
||||
Reference in New Issue
Block a user