feat(admin): 仪表盘重构、代理线路修复与后台体验优化
Some checks failed
lotteryadmin CI / build (push) Has been cancelled

- 各角色仪表盘:KPI 可点击跳转、快捷入口、去重冗余区块,代理/站点降级 analytics
- 代理线路:创建玩家权限、侧栏搜索、深链 URL、档案保存与删除预检等修复
- 统一密码最短 6 位;代理模块表格/侧栏背景色一致(admin-table-inset)
- 报表、钱包、对账、开奖、结算、配置等模块结构与 i18n 同步更新
This commit is contained in:
2026-06-26 14:39:22 +08:00
parent 7d075ceb62
commit 0d984b00f0
126 changed files with 7423 additions and 7084 deletions

View File

@@ -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({