feat: 增加管理端多语言与多模块界面国际化支持
This commit is contained in:
@@ -4,6 +4,7 @@ import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState, type ReactElement, type ReactNode } from "react";
|
||||
import { format } from "date-fns";
|
||||
import { zhCN } from "date-fns/locale";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ClipboardList,
|
||||
@@ -30,7 +31,7 @@ import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance
|
||||
import type { AdminRiskPoolRow } from "@/types/api/admin-risk";
|
||||
import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
|
||||
|
||||
type HotPlayTab = "4D" | "3D" | "2D" | "特别";
|
||||
type HotPlayTab = "4D" | "3D" | "2D" | "special";
|
||||
|
||||
type SoldOutBuckets = {
|
||||
d4: number;
|
||||
@@ -66,7 +67,7 @@ function formatSignedMoneyMinor(minor: number, currencyCode: string | null): str
|
||||
return `${s}${formatMoneyMinor(Math.abs(minor), currencyCode)}`;
|
||||
}
|
||||
|
||||
/** 与后端 {@see AdminDashboardSnapshotBuilder::soldOutBucketKey} 维度对齐 */
|
||||
/** Aligned with the bucket dimensions used by AdminDashboardSnapshotBuilder::soldOutBucketKey. */
|
||||
function poolPlayCategory(normalizedNumber: string): HotPlayTab | "other" {
|
||||
const raw = normalizedNumber.trim();
|
||||
const digits = raw.replace(/\D/g, "");
|
||||
@@ -74,7 +75,7 @@ function poolPlayCategory(normalizedNumber: string): HotPlayTab | "other" {
|
||||
const hasLetter = /[A-Za-z]/.test(raw);
|
||||
|
||||
if (hasLetter && digitLen < 3) {
|
||||
return "特别";
|
||||
return "special";
|
||||
}
|
||||
if (digitLen >= 4) {
|
||||
return "4D";
|
||||
@@ -86,7 +87,7 @@ function poolPlayCategory(normalizedNumber: string): HotPlayTab | "other" {
|
||||
return "2D";
|
||||
}
|
||||
if (hasLetter) {
|
||||
return "特别";
|
||||
return "special";
|
||||
}
|
||||
return "other";
|
||||
}
|
||||
@@ -99,6 +100,7 @@ function topPoolsForTab(pools: AdminRiskPoolRow[], tab: HotPlayTab): AdminRiskPo
|
||||
}
|
||||
|
||||
function RiskSemiGauge({ pct }: { pct: number }): ReactElement {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const v = Math.min(100, Math.max(0, pct));
|
||||
const r = 76;
|
||||
const arcLen = Math.PI * r;
|
||||
@@ -126,13 +128,14 @@ function RiskSemiGauge({ pct }: { pct: number }): ReactElement {
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-end pb-1 text-center">
|
||||
<p className="text-lg font-bold tabular-nums text-[#1a365d]">{v.toFixed(2)}%</p>
|
||||
<p className="text-[11px] leading-tight text-muted-foreground">封顶占用</p>
|
||||
<p className="text-[11px] leading-tight text-muted-foreground">{t("capUsage")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HotBarChart({ rows }: { rows: AdminRiskPoolRow[] }): ReactElement {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const maxU = Math.max(0.0001, ...rows.map((r) => r.usage_ratio ?? 0));
|
||||
|
||||
return (
|
||||
@@ -142,10 +145,10 @@ function HotBarChart({ rows }: { rows: AdminRiskPoolRow[] }): ReactElement {
|
||||
className="pointer-events-none absolute bottom-6 left-0 top-2 w-6 rotate-180 text-[10px] leading-tight text-muted-foreground [writing-mode:vertical-rl]"
|
||||
aria-hidden
|
||||
>
|
||||
占用率
|
||||
{t("capUsage")}
|
||||
</span>
|
||||
{rows.length === 0 ? (
|
||||
<p className="w-full pb-6 text-center text-sm text-muted-foreground">该维度暂无池数据</p>
|
||||
<p className="w-full pb-6 text-center text-sm text-muted-foreground">{t("noPoolData")}</p>
|
||||
) : (
|
||||
rows.map((row) => {
|
||||
const u = row.usage_ratio ?? 0;
|
||||
@@ -170,25 +173,26 @@ function HotBarChart({ rows }: { rows: AdminRiskPoolRow[] }): ReactElement {
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1.5 text-center text-[11px] text-muted-foreground">号码(按占用率)</p>
|
||||
<p className="mt-1.5 text-center text-[11px] text-muted-foreground">{t("numbersByUsage")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SoldOutDonut({ buckets }: { buckets: SoldOutBuckets }): ReactElement {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const entries: { key: keyof SoldOutBuckets; label: string; color: string }[] = [
|
||||
{ key: "d4", label: "4D", color: "oklch(0.32 0.08 260)" },
|
||||
{ key: "d3", label: "3D", color: "oklch(0.48 0.12 250)" },
|
||||
{ key: "d2", label: "2D", color: "oklch(0.78 0.14 95)" },
|
||||
{ key: "special", label: "特别号", color: "oklch(0.55 0.22 25)" },
|
||||
{ key: "other", label: "其他", color: "oklch(0.62 0.16 145)" },
|
||||
{ key: "d4", label: t("soldOutBuckets.d4"), color: "oklch(0.32 0.08 260)" },
|
||||
{ key: "d3", label: t("soldOutBuckets.d3"), color: "oklch(0.48 0.12 250)" },
|
||||
{ key: "d2", label: t("soldOutBuckets.d2"), color: "oklch(0.78 0.14 95)" },
|
||||
{ key: "special", label: t("soldOutBuckets.special"), color: "oklch(0.55 0.22 25)" },
|
||||
{ key: "other", label: t("soldOutBuckets.other"), color: "oklch(0.62 0.16 145)" },
|
||||
];
|
||||
const total = entries.reduce((s, e) => s + buckets[e.key], 0);
|
||||
|
||||
if (total === 0) {
|
||||
return (
|
||||
<div className="flex min-h-[200px] flex-col items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<p>暂无售罄号码</p>
|
||||
<p>{t("noSoldOutNumbers")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -227,7 +231,7 @@ function SoldOutDonut({ buckets }: { buckets: SoldOutBuckets }): ReactElement {
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center">
|
||||
<p className="text-2xl font-bold tabular-nums text-[#1a365d]">{total}</p>
|
||||
<p className="text-[11px] text-muted-foreground">售罄合计</p>
|
||||
<p className="text-[11px] text-muted-foreground">{t("soldOutTotal")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="min-w-0 flex-1 space-y-2 text-sm">
|
||||
@@ -246,6 +250,7 @@ function SoldOutDonut({ buckets }: { buckets: SoldOutBuckets }): ReactElement {
|
||||
}
|
||||
|
||||
export function DashboardConsole(): ReactElement {
|
||||
const { t } = useTranslation(["dashboard", "common"]);
|
||||
const [todayLabel] = useState(() => format(new Date(), "yyyy-MM-dd EEEE", { locale: zhCN }));
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
@@ -304,21 +309,21 @@ export function DashboardConsole(): ReactElement {
|
||||
|
||||
const noticeParts: string[] = d.warnings.map((w) => w.message);
|
||||
if (d.resolved_draw != null && !d.capabilities.draw_finance_risk) {
|
||||
noticeParts.push("当前账号无开奖查看/管理权限,财务与风控数据未返回。");
|
||||
noticeParts.push(t("warnings.drawPermission"));
|
||||
}
|
||||
if (d.hall != null && !d.capabilities.wallet_transfer_view) {
|
||||
noticeParts.push("当前账号无钱包对账查看权限,异常转账计数未返回。");
|
||||
noticeParts.push(t("warnings.walletPermission"));
|
||||
}
|
||||
setNotice(noticeParts.length > 0 ? noticeParts.join(" ") : null);
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e instanceof LotteryApiBizError ? e.message : "加载失败,请检查 API 与登录状态。";
|
||||
e instanceof LotteryApiBizError ? e.message : t("warnings.loadFailed");
|
||||
setError(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
const t = window.setTimeout(() => {
|
||||
@@ -335,28 +340,27 @@ export function DashboardConsole(): ReactElement {
|
||||
const hallStatusLabel = hall?.status ?? "—";
|
||||
const isOpenLike =
|
||||
hallStatusLabel.toLowerCase().includes("open") ||
|
||||
hallStatusLabel.includes("开售") ||
|
||||
hallStatusLabel.includes("开放");
|
||||
hallStatusLabel.toLowerCase().includes("sale");
|
||||
|
||||
const quickLinks: { href: string; label: string; icon: ReactNode }[] = [
|
||||
{ href: "/admin/draws", label: "创建期计划", icon: <Diamond className="size-5" /> },
|
||||
{ href: "/admin/draws", label: "开售 / 期号", icon: <Ticket className="size-5" /> },
|
||||
{ href: "/admin/draws", label: t("quickLinks.createDrawPlan"), icon: <Diamond className="size-5" /> },
|
||||
{ href: "/admin/draws", label: t("quickLinks.drawSchedule"), icon: <Ticket className="size-5" /> },
|
||||
{
|
||||
href: drawId != null ? `/admin/draws/${drawId}/results` : "/admin/draws",
|
||||
label: "开奖结果",
|
||||
label: t("quickLinks.results"),
|
||||
icon: <FileSearch className="size-5" />,
|
||||
},
|
||||
{ href: "/admin/tickets", label: "注单管理", icon: <Shield className="size-5" /> },
|
||||
{ href: "/admin/wallet/transactions", label: "钱包流水", icon: <Wallet className="size-5" /> },
|
||||
{ href: "/admin/reports", label: "报表中心", icon: <FileSpreadsheet className="size-5" /> },
|
||||
{ href: "/admin/audit-logs", label: "审计日志", icon: <ScrollText className="size-5" /> },
|
||||
{ href: "/admin/tickets", label: t("quickLinks.tickets"), icon: <Shield className="size-5" /> },
|
||||
{ href: "/admin/wallet/transactions", label: t("quickLinks.walletTransactions"), icon: <Wallet className="size-5" /> },
|
||||
{ href: "/admin/reports", label: t("quickLinks.reports"), icon: <FileSpreadsheet className="size-5" /> },
|
||||
{ href: "/admin/audit-logs", label: t("quickLinks.auditLogs"), icon: <ScrollText className="size-5" /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6 pb-10">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-[#1a365d]">仪表盘</h1>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-[#1a365d]">{t("title")}</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">{todayLabel}</span>
|
||||
@@ -369,26 +373,26 @@ export function DashboardConsole(): ReactElement {
|
||||
onClick={() => void load(true)}
|
||||
>
|
||||
<RefreshCw className={refreshing ? "size-4 animate-spin" : "size-4"} />
|
||||
刷新
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<Alert className="border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30">
|
||||
<AlertTitle>提示</AlertTitle>
|
||||
<AlertTitle>{t("notice")}</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{notice && !error ? (
|
||||
<Alert className="border-sky-200 bg-sky-50 dark:border-sky-900/50 dark:bg-sky-950/30">
|
||||
<AlertTitle>提示</AlertTitle>
|
||||
<AlertTitle>{t("notice")}</AlertTitle>
|
||||
<AlertDescription>{notice}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{/* Row 1 — 核心财务 KPI */}
|
||||
{/* Row 1 - Core finance KPI */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{loading ? (
|
||||
Array.from({ length: 3 }).map((_, i) => (
|
||||
@@ -407,13 +411,13 @@ export function DashboardConsole(): ReactElement {
|
||||
<Wallet className="size-5" aria-hidden />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-slate-600">当期投注总额</p>
|
||||
<p className="text-sm font-medium text-slate-600">{t("todayBetTotal")}</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums text-[#1a365d]">
|
||||
{finance ? formatMoneyMinor(finance.total_bet_minor, currency) : "—"}
|
||||
</p>
|
||||
<p className="mt-2 flex items-center gap-1 text-xs text-emerald-600">
|
||||
<TrendingUp className="size-3.5 shrink-0" aria-hidden />
|
||||
当前大厅期财务汇总
|
||||
{t("currentDrawFinanceSummary")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -424,13 +428,13 @@ export function DashboardConsole(): ReactElement {
|
||||
<Gift className="size-5" aria-hidden />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-slate-600">当期派彩</p>
|
||||
<p className="text-sm font-medium text-slate-600">{t("currentPayout")}</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums text-[#1a365d]">
|
||||
{finance ? formatMoneyMinor(finance.total_payout_minor, currency) : "—"}
|
||||
</p>
|
||||
<p className="mt-2 flex items-center gap-1 text-xs text-emerald-600">
|
||||
<TrendingUp className="size-3.5 shrink-0" aria-hidden />
|
||||
中奖派彩 + Jackpot
|
||||
{t("payoutSummary")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -441,13 +445,13 @@ export function DashboardConsole(): ReactElement {
|
||||
<TrendingUp className="size-5" aria-hidden />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-slate-600">当期平台盈亏</p>
|
||||
<p className="text-sm font-medium text-slate-600">{t("currentProfit")}</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums text-[#1a365d]">
|
||||
{finance ? formatSignedMoneyMinor(finance.approx_house_gross_minor, currency) : "—"}
|
||||
</p>
|
||||
<p className="mt-2 flex items-center gap-1 text-xs text-emerald-600">
|
||||
<TrendingUp className="size-3.5 shrink-0" aria-hidden />
|
||||
投注 − 派彩(近似)
|
||||
{t("profitFormula")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -456,7 +460,7 @@ export function DashboardConsole(): ReactElement {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 2 — 期号 / 投注 / 风险表 */}
|
||||
{/* Row 2 - Draw / betting / risk */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{loading ? (
|
||||
Array.from({ length: 3 }).map((_, i) => (
|
||||
@@ -472,10 +476,10 @@ export function DashboardConsole(): ReactElement {
|
||||
<Ticket className="size-5" aria-hidden />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-slate-600">当前期号</p>
|
||||
<p className="text-sm font-medium text-slate-600">{t("currentDraw")}</p>
|
||||
<p className="mt-1 font-mono text-2xl font-bold text-[#c41e3a]">{hall?.draw_no ?? "—"}</p>
|
||||
<p className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>第 {hall?.sequence_no ?? "—"} 期</span>
|
||||
<span>{t("drawSequence", { sequence: hall?.sequence_no ?? "—" })}</span>
|
||||
<span className="hidden sm:inline">·</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span
|
||||
@@ -495,7 +499,7 @@ export function DashboardConsole(): ReactElement {
|
||||
)}
|
||||
href={`/admin/draws/${drawId}`}
|
||||
>
|
||||
期号详情
|
||||
{t("drawDetails")}
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -507,12 +511,12 @@ export function DashboardConsole(): ReactElement {
|
||||
<Wallet className="size-5" aria-hidden />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-slate-600">本期注单笔数</p>
|
||||
<p className="text-sm font-medium text-slate-600">{t("ticketCount")}</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums text-[#1a365d]">
|
||||
{finance != null ? finance.ticket_item_count.toLocaleString("zh-CN") : "—"}
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
关联投注额{" "}
|
||||
{t("relatedBetAmount")}{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{finance ? formatMoneyMinor(finance.total_bet_minor, currency) : "—"}
|
||||
</span>
|
||||
@@ -526,10 +530,12 @@ export function DashboardConsole(): ReactElement {
|
||||
<Shield className="size-5" aria-hidden />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 text-center sm:text-left">
|
||||
<p className="text-sm font-medium text-slate-600">风险封顶占用</p>
|
||||
<p className="text-sm font-medium text-slate-600">{t("riskCapUsage")}</p>
|
||||
<p className="mt-1 text-xs tabular-nums text-muted-foreground">
|
||||
已占用 {formatMoneyMinor(riskLocked, currency)} / 封顶{" "}
|
||||
{formatMoneyMinor(riskCap, currency)}
|
||||
{t("lockedAndCap", {
|
||||
locked: formatMoneyMinor(riskLocked, currency),
|
||||
cap: formatMoneyMinor(riskCap, currency),
|
||||
})}
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<RiskSemiGauge pct={usagePct} />
|
||||
@@ -542,7 +548,7 @@ export function DashboardConsole(): ReactElement {
|
||||
)}
|
||||
href={`/admin/risk/draws/${drawId}/occupancy`}
|
||||
>
|
||||
占用明细
|
||||
{t("occupancyDetails")}
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -552,30 +558,35 @@ export function DashboardConsole(): ReactElement {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 3 — 图表 */}
|
||||
{/* Row 3 - Charts */}
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<Card className="border-slate-200/90 shadow-sm">
|
||||
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-2 space-y-0 pb-2">
|
||||
<div>
|
||||
<CardTitle className="text-base font-semibold text-[#1a365d]">热门号码 Top 10</CardTitle>
|
||||
<CardTitle className="text-base font-semibold text-[#1a365d]">{t("hotNumbersTop10")}</CardTitle>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div role="tablist" aria-label="玩法维度" className="flex gap-1 border-b border-transparent">
|
||||
{(["4D", "3D", "2D", "特别"] as const).map((tab) => (
|
||||
<div role="tablist" aria-label={t("playDimension")} className="flex gap-1 border-b border-transparent">
|
||||
{([
|
||||
{ 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}
|
||||
key={tab.value}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={hotTab === tab}
|
||||
aria-selected={hotTab === tab.value}
|
||||
className={cn(
|
||||
"-mb-px border-b-2 px-2.5 py-1 text-sm font-medium transition-colors",
|
||||
hotTab === tab
|
||||
hotTab === tab.value
|
||||
? "border-[#c41e3a] text-[#c41e3a]"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
onClick={() => setHotTab(tab)}
|
||||
onClick={() => setHotTab(tab.value)}
|
||||
>
|
||||
{tab}
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -587,7 +598,7 @@ export function DashboardConsole(): ReactElement {
|
||||
"h-7 border-[#2563eb]/30 px-2 text-xs text-[#2563eb] hover:bg-[#2563eb]/5",
|
||||
)}
|
||||
>
|
||||
查看全部
|
||||
{t("actions.viewAll", { ns: "common" })}
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -604,7 +615,7 @@ export function DashboardConsole(): ReactElement {
|
||||
<Card className="border-slate-200/90 shadow-sm">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<div>
|
||||
<CardTitle className="text-base font-semibold text-[#1a365d]">售罄分布</CardTitle>
|
||||
<CardTitle className="text-base font-semibold text-[#1a365d]">{t("soldOutDistribution")}</CardTitle>
|
||||
</div>
|
||||
{drawId != null ? (
|
||||
<Link
|
||||
@@ -614,7 +625,7 @@ export function DashboardConsole(): ReactElement {
|
||||
"h-7 border-[#2563eb]/30 px-2 text-xs text-[#2563eb] hover:bg-[#2563eb]/5",
|
||||
)}
|
||||
>
|
||||
查看全部
|
||||
{t("actions.viewAll", { ns: "common" })}
|
||||
</Link>
|
||||
) : null}
|
||||
</CardHeader>
|
||||
@@ -624,13 +635,13 @@ export function DashboardConsole(): ReactElement {
|
||||
) : soldOutBuckets ? (
|
||||
<SoldOutDonut buckets={soldOutBuckets} />
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">暂无数据</p>
|
||||
<p className="text-sm text-muted-foreground">{t("states.noData", { ns: "common" })}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Row 4 — 待办 */}
|
||||
{/* Row 4 - To-do */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="flex flex-col justify-between gap-4 rounded-xl border border-slate-200/90 bg-white p-5 shadow-sm sm:flex-row sm:items-center">
|
||||
<div className="flex gap-4">
|
||||
@@ -638,7 +649,7 @@ export function DashboardConsole(): ReactElement {
|
||||
<ClipboardList className="size-5" aria-hidden />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-600">待审核开奖</p>
|
||||
<p className="text-sm font-medium text-slate-600">{t("pendingReviewResults")}</p>
|
||||
<p className="mt-1 text-4xl font-bold tabular-nums text-[#c41e3a]">
|
||||
{pendingReview ?? "—"}
|
||||
</p>
|
||||
@@ -652,7 +663,7 @@ export function DashboardConsole(): ReactElement {
|
||||
"shrink-0 border-[#c41e3a] text-[#c41e3a] hover:bg-[#c41e3a]/5",
|
||||
)}
|
||||
>
|
||||
立即审核
|
||||
{t("actions.reviewNow", { ns: "common" })}
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -662,7 +673,7 @@ export function DashboardConsole(): ReactElement {
|
||||
<AlertTriangle className="size-5" aria-hidden />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-600">异常转账单</p>
|
||||
<p className="text-sm font-medium text-slate-600">{t("abnormalTransferOrders")}</p>
|
||||
<p className="mt-1 text-4xl font-bold tabular-nums text-[#c41e3a]">
|
||||
{abnormalTransferTotal ?? "—"}
|
||||
</p>
|
||||
@@ -675,12 +686,12 @@ export function DashboardConsole(): ReactElement {
|
||||
"shrink-0 border-[#c41e3a] text-[#c41e3a] hover:bg-[#c41e3a]/5",
|
||||
)}
|
||||
>
|
||||
查看转账单
|
||||
{t("viewTransferOrders")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 5 — 快捷入口 */}
|
||||
{/* Row 5 - Quick links */}
|
||||
<Card className="border-slate-200/90 shadow-sm">
|
||||
<CardContent className="flex flex-wrap justify-center gap-3 py-6 sm:gap-6">
|
||||
{quickLinks.map((q) => (
|
||||
|
||||
Reference in New Issue
Block a user