feat(dashboard, i18n): 增强玩家身份信息展示并完善多语言支持
更新仪表盘相关组件,采用新的玩家身份信息字段(Player Identity Columns),提升数据展示的清晰度与可读性。 优化奖池记录(Jackpot Records)中的玩家信息展示方式,便于快速识别玩家身份。 改进结算明细(Settlement Details)页面的玩家身份展示,提升数据追踪与核对效率。 更新玩家注单(Player Tickets)与钱包交易(Wallet Transactions)相关界面,统一使用新的玩家身份信息展示逻辑。 在英文、尼泊尔语与中文语言包中新增玩家相关术语翻译,增强多语言支持。 提升系统整体用户体验,确保各模块中的玩家信息展示更加一致、直观。
This commit is contained in:
@@ -41,8 +41,8 @@ import {
|
||||
CapUsageBar,
|
||||
FinanceStructureChart,
|
||||
HotUsageBars,
|
||||
PayoutPanelSnapshot,
|
||||
ResultBatchQueueSummary,
|
||||
PlatformLifetimePayoutSnapshot,
|
||||
DashboardPanelCard,
|
||||
SettlementStatusChart,
|
||||
} from "@/modules/dashboard/dashboard-visuals";
|
||||
@@ -55,6 +55,8 @@ import { cn } from "@/lib/utils";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type {
|
||||
AdminDashboardDrawPanel,
|
||||
AdminDashboardLifetimeFinance,
|
||||
AdminDashboardPlatformRisk,
|
||||
AdminDashboardResultBatchQueue,
|
||||
} from "@/types/api/admin-dashboard";
|
||||
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
|
||||
@@ -151,6 +153,10 @@ export function DashboardConsole(): ReactElement {
|
||||
const [resultBatchQueue, setResultBatchQueue] = useState<AdminDashboardResultBatchQueue | null>(
|
||||
null,
|
||||
);
|
||||
const [lifetimeFinance, setLifetimeFinance] = useState<AdminDashboardLifetimeFinance | null>(
|
||||
null,
|
||||
);
|
||||
const [platformRisk, setPlatformRisk] = useState<AdminDashboardPlatformRisk | null>(null);
|
||||
const [riskLocked, setRiskLocked] = useState(0);
|
||||
const [riskCap, setRiskCap] = useState(0);
|
||||
const [hotPoolSample, setHotPoolSample] = useState<AdminRiskPoolRow[]>([]);
|
||||
@@ -190,6 +196,8 @@ export function DashboardConsole(): ReactElement {
|
||||
setCapabilities(null);
|
||||
setDrawPanel(null);
|
||||
setResultBatchQueue(null);
|
||||
setLifetimeFinance(null);
|
||||
setPlatformRisk(null);
|
||||
setDrawId(null);
|
||||
setRiskLocked(0);
|
||||
setRiskCap(0);
|
||||
@@ -208,9 +216,11 @@ export function DashboardConsole(): ReactElement {
|
||||
if (d.finance != null) {
|
||||
setFinance(d.finance);
|
||||
}
|
||||
setResultBatchQueue(d.result_batch_queue);
|
||||
setLifetimeFinance(d.lifetime_finance);
|
||||
setPlatformRisk(d.platform_risk);
|
||||
if (d.draw != null) {
|
||||
setDrawPanel(d.draw);
|
||||
setResultBatchQueue(d.result_batch_queue);
|
||||
}
|
||||
if (d.risk != null) {
|
||||
setRiskLocked(d.risk.locked_amount);
|
||||
@@ -235,14 +245,16 @@ export function DashboardConsole(): ReactElement {
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [load]);
|
||||
|
||||
const currency = finance?.currency_code ?? null;
|
||||
const currency =
|
||||
lifetimeFinance?.currency_code ?? finance?.currency_code ?? null;
|
||||
const canFinance = capabilities?.draw_finance_risk ?? false;
|
||||
const usagePct = riskCap > 0 ? (riskLocked / riskCap) * 100 : 0;
|
||||
const platformLocked = platformRisk?.locked_amount ?? 0;
|
||||
const platformCap = platformRisk?.cap_amount ?? 0;
|
||||
const platformUsagePct = platformRisk?.usage_percent ?? 0;
|
||||
|
||||
const hotRows = useMemo(() => topPoolsForTab(hotPoolSample, hotTab), [hotPoolSample, hotTab]);
|
||||
|
||||
const pendingReviewTotal = resultBatchQueue?.pending_review_total ?? 0;
|
||||
const currentDrawPending = drawPanel?.result_batch_counts.pending_review ?? 0;
|
||||
|
||||
const analytics = useDashboardAnalytics({ enabled: canFinance, playOptions });
|
||||
const showAnalytics = canFinance;
|
||||
@@ -322,11 +334,7 @@ export function DashboardConsole(): ReactElement {
|
||||
loading={loading}
|
||||
>
|
||||
{resultBatchQueue != null ? (
|
||||
<ResultBatchQueueSummary
|
||||
queue={resultBatchQueue}
|
||||
currentDrawPending={currentDrawPending}
|
||||
compact
|
||||
/>
|
||||
<ResultBatchQueueSummary queue={resultBatchQueue} compact />
|
||||
) : null}
|
||||
</DashboardPanelCard>
|
||||
|
||||
@@ -348,53 +356,62 @@ export function DashboardConsole(): ReactElement {
|
||||
</DashboardPanelCard>
|
||||
|
||||
<DashboardPanelCard
|
||||
href={drawScopedHref(drawId, "/risk/occupancy", "/admin/risk")}
|
||||
href="/admin/risk"
|
||||
title={t("riskCapUsage")}
|
||||
value={`${usagePct.toFixed(1)}%`}
|
||||
subtitle={t("lockedAndCap", {
|
||||
locked: formatMoneyMinor(riskLocked, currency),
|
||||
cap: formatMoneyMinor(riskCap, currency),
|
||||
value={`${platformUsagePct.toFixed(1)}%`}
|
||||
subtitle={t("platformLockedAndCap", {
|
||||
locked: formatMoneyMinor(platformLocked, currency),
|
||||
cap: formatMoneyMinor(platformCap, currency),
|
||||
})}
|
||||
actionLabel={t("occupancyDetails")}
|
||||
icon={<Shield className="size-5" aria-hidden />}
|
||||
accent={
|
||||
usagePct >= 90 ? "destructive" : usagePct >= 70 ? "primary" : "muted"
|
||||
platformUsagePct >= 90
|
||||
? "destructive"
|
||||
: platformUsagePct >= 70
|
||||
? "primary"
|
||||
: "muted"
|
||||
}
|
||||
loading={loading}
|
||||
>
|
||||
<CapUsageBar
|
||||
locked={riskLocked}
|
||||
cap={riskCap}
|
||||
usagePct={usagePct}
|
||||
formatMoney={formatMoneyMinor}
|
||||
currency={currency}
|
||||
compact
|
||||
/>
|
||||
{platformRisk != null ? (
|
||||
<CapUsageBar
|
||||
locked={platformLocked}
|
||||
cap={platformCap}
|
||||
usagePct={platformUsagePct}
|
||||
formatMoney={formatMoneyMinor}
|
||||
currency={currency}
|
||||
compact
|
||||
/>
|
||||
) : null}
|
||||
</DashboardPanelCard>
|
||||
|
||||
<DashboardPanelCard
|
||||
href={drawScopedHref(drawId, "/finance")}
|
||||
href="/admin/reports"
|
||||
title={t("payoutComposition")}
|
||||
value={
|
||||
finance
|
||||
? formatMoneyMinor(finance.total_payout_minor, currency)
|
||||
lifetimeFinance
|
||||
? formatMoneyMinor(lifetimeFinance.total_payout_minor, currency)
|
||||
: "—"
|
||||
}
|
||||
subtitle={
|
||||
finance
|
||||
? t("orderAndTicket", {
|
||||
orders: finance.order_count,
|
||||
tickets: finance.ticket_item_count,
|
||||
lifetimeFinance
|
||||
? t("platformOrderAndTicket", {
|
||||
orders: lifetimeFinance.order_count,
|
||||
tickets: lifetimeFinance.ticket_item_count,
|
||||
})
|
||||
: t("states.noData", { ns: "common" })
|
||||
}
|
||||
actionLabel={t("detailsShort")}
|
||||
actionLabel={t("actions.viewAll", { ns: "common" })}
|
||||
icon={<Wallet className="size-5" aria-hidden />}
|
||||
accent="primary"
|
||||
loading={loading}
|
||||
>
|
||||
{finance ? (
|
||||
<PayoutPanelSnapshot finance={finance} formatMoney={formatMoneyMinor} />
|
||||
{lifetimeFinance ? (
|
||||
<PlatformLifetimePayoutSnapshot
|
||||
finance={lifetimeFinance}
|
||||
formatMoney={formatMoneyMinor}
|
||||
/>
|
||||
) : null}
|
||||
</DashboardPanelCard>
|
||||
</div>
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
} from "@/modules/dashboard/dashboard-chart-config";
|
||||
import { DashboardChartEmpty } from "@/modules/dashboard/dashboard-chart-empty";
|
||||
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
|
||||
import type { AdminDashboardLifetimeFinance } from "@/types/api/admin-dashboard";
|
||||
import type { AdminRiskPoolRow } from "@/types/api/admin-risk";
|
||||
import type {
|
||||
AdminDashboardDrawPanel,
|
||||
@@ -976,15 +977,13 @@ export function ResultBatchProgress({
|
||||
|
||||
export function ResultBatchQueueSummary({
|
||||
queue,
|
||||
currentDrawPending,
|
||||
compact = false,
|
||||
}: {
|
||||
queue: AdminDashboardResultBatchQueue;
|
||||
currentDrawPending: number;
|
||||
compact?: boolean;
|
||||
}): ReactElement {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const { pending_review_total, pending_draw_count } = queue;
|
||||
const { pending_review_total, pending_draw_count, published_total, batch_total } = queue;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2 text-center">
|
||||
@@ -999,27 +998,89 @@ export function ResultBatchQueueSummary({
|
||||
</p>
|
||||
<p className="mt-0.5 text-[10px] text-muted-foreground">{t("batchPending")}</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-sky-500/8 px-2 py-2 ring-1 ring-sky-500/15">
|
||||
<div className="rounded-lg bg-emerald-500/8 px-2 py-2 ring-1 ring-emerald-500/15">
|
||||
<p
|
||||
className={cn(
|
||||
"font-bold tabular-nums text-sky-800 dark:text-sky-300",
|
||||
"font-bold tabular-nums text-emerald-700 dark:text-emerald-400",
|
||||
compact ? "text-lg" : "text-2xl",
|
||||
)}
|
||||
>
|
||||
{pending_draw_count}
|
||||
{published_total}
|
||||
</p>
|
||||
<p className="mt-0.5 text-[10px] text-muted-foreground">{t("batchPendingDraws")}</p>
|
||||
<p className="mt-0.5 text-[10px] text-muted-foreground">{t("batchPublished")}</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-muted/50 px-2 py-2 ring-1 ring-border/60">
|
||||
<p className={cn("font-bold tabular-nums text-foreground", compact ? "text-lg" : "text-2xl")}>
|
||||
{currentDrawPending}
|
||||
{batch_total}
|
||||
</p>
|
||||
<p className="mt-0.5 text-[10px] text-muted-foreground">
|
||||
{pending_draw_count > 0
|
||||
? t("batchPendingDrawsCount", { count: pending_draw_count })
|
||||
: t("batchTotal")}
|
||||
</p>
|
||||
<p className="mt-0.5 text-[10px] text-muted-foreground">{t("batchCurrentDrawPending")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PlatformLifetimePayoutSnapshot({
|
||||
finance,
|
||||
formatMoney,
|
||||
}: {
|
||||
finance: AdminDashboardLifetimeFinance;
|
||||
formatMoney: MoneyFormatter;
|
||||
}): ReactElement {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const currency = finance.currency_code;
|
||||
const bet = finance.total_bet_minor;
|
||||
const win = finance.total_win_minor;
|
||||
const jackpot = finance.total_jackpot_minor;
|
||||
const hasPayout = win + jackpot > 0;
|
||||
|
||||
if (bet <= 0 && !hasPayout) {
|
||||
return <DashboardChartEmpty message={t("platformNoFinanceActivity")} compact />;
|
||||
}
|
||||
|
||||
const cells = [
|
||||
{ key: "bet", label: t("platformBetTotal"), amount: bet, emphasize: bet > 0 },
|
||||
{ key: "win", label: t("winPayout"), amount: win, emphasize: win > 0 },
|
||||
{ key: "jackpot", label: t("jackpotPayout"), amount: jackpot, emphasize: jackpot > 0 },
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-2 text-center">
|
||||
{cells.map((cell) => (
|
||||
<div
|
||||
key={cell.key}
|
||||
className={cn(
|
||||
"rounded-lg px-1.5 py-2 ring-1",
|
||||
cell.emphasize
|
||||
? "bg-primary/6 ring-primary/15"
|
||||
: "bg-muted/30 ring-border/50",
|
||||
)}
|
||||
>
|
||||
<p className="text-[10px] leading-tight text-muted-foreground">{cell.label}</p>
|
||||
<p
|
||||
className={cn(
|
||||
"mt-1 text-[11px] font-bold tabular-nums leading-tight",
|
||||
cell.emphasize ? "text-foreground" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{formatMoney(cell.amount, currency)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{!hasPayout ? (
|
||||
<p className="rounded-lg bg-muted/25 px-2 py-2 text-center text-[11px] text-muted-foreground ring-1 ring-border/40">
|
||||
{t("platformNoPayoutYet")}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettlementStatusChart({
|
||||
finance,
|
||||
}: {
|
||||
|
||||
Reference in New Issue
Block a user