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:
420
src/modules/reports/report-preview-tables.tsx
Normal file
420
src/modules/reports/report-preview-tables.tsx
Normal file
@@ -0,0 +1,420 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { adminAgentDisplayLabel } from "@/components/admin/admin-agent-columns";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||
import { AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import { DrawStatusBadge } from "@/modules/draws/draw-status-badge";
|
||||
import { drawStatusLabel } from "@/modules/draws/draw-display";
|
||||
import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
formatPlainMoney,
|
||||
formatUsagePercent,
|
||||
optionText,
|
||||
signedProfitCell,
|
||||
type ReportResult,
|
||||
} from "@/modules/reports/reports-utils";
|
||||
import type { ReportDefinition } from "@/modules/reports/reports-definitions";
|
||||
|
||||
type ReportPreviewTableProps = {
|
||||
report: ReportDefinition;
|
||||
result: ReportResult | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
drawNoLabel?: string;
|
||||
displayCurrency: string;
|
||||
};
|
||||
|
||||
export function ReportPreviewTable({
|
||||
report,
|
||||
result,
|
||||
loading,
|
||||
error,
|
||||
drawNoLabel = "",
|
||||
displayCurrency,
|
||||
}: ReportPreviewTableProps) {
|
||||
const { t } = useTranslation(["reports", "common"]);
|
||||
const playCodeLabel = useAdminPlayCodeLabel();
|
||||
const formatTs = useAdminDateTimeFormatter();
|
||||
|
||||
if (!report.connected) {
|
||||
return <AdminNoResourceState message={t("backendPending")} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<AdminTableLoadingRow colSpan={8} />
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-destructive">
|
||||
{error}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
if (!result || result.key !== report.key || result.rows.length === 0) {
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<AdminTableNoResourceRow colSpan={8} message={t("preview.empty")} />
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
switch (result.key) {
|
||||
case "draw_profit":
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("preview.columns.drawProfit.primary")}</TableHead>
|
||||
<TableHead>{t("preview.columns.drawProfit.secondary")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.drawProfit.metricA")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.drawProfit.metricB")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.drawProfit.metricC")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.drawProfit.status")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.drawProfit.extra")}</TableHead>
|
||||
<TableHead>{t("preview.columns.drawProfit.time")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">{result.raw.draw_no}</TableCell>
|
||||
<TableCell>
|
||||
<DrawStatusBadge
|
||||
status={result.raw.draw_status}
|
||||
label={drawStatusLabel(result.raw.draw_status, t)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{result.raw.order_count}</TableCell>
|
||||
<TableCell className="text-right">{result.raw.ticket_item_count}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(result.raw.total_bet_minor, result.raw.currency_code)}
|
||||
</TableCell>
|
||||
<TableCell className={signedProfitCell(result.raw.approx_house_gross_minor, result.raw.currency_code)}>
|
||||
{formatPlainMoney(result.raw.approx_house_gross_minor, result.raw.currency_code)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(result.raw.total_payout_minor, result.raw.currency_code)}
|
||||
</TableCell>
|
||||
<TableCell>—</TableCell>
|
||||
</TableRow>
|
||||
{result.raw.settlement_batches.length > 0 ? (
|
||||
<TableRow className="bg-muted/20">
|
||||
<TableCell colSpan={8} className="py-2 text-xs font-medium text-muted-foreground">
|
||||
{t("preview.sections.settlementBatches")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{result.raw.settlement_batches.map((batch) => (
|
||||
<TableRow key={batch.id} className="bg-muted/10">
|
||||
<TableCell>#{batch.id}</TableCell>
|
||||
<TableCell>
|
||||
<AdminStatusBadge status={batch.status}>{batch.status}</AdminStatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{batch.total_ticket_count}</TableCell>
|
||||
<TableCell className="text-right">{batch.total_win_count}</TableCell>
|
||||
<TableCell className="text-right">—</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(batch.total_payout_amount, result.raw.currency_code)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(batch.total_jackpot_payout_amount, result.raw.currency_code)}
|
||||
</TableCell>
|
||||
<TableCell>{formatTs(batch.finished_at)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
case "daily_profit":
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("preview.columns.dailyProfit.primary")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.dailyProfit.metricA")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.dailyProfit.metricB")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.dailyProfit.metricC")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{result.raw.map((item) => (
|
||||
<TableRow key={item.business_date}>
|
||||
<TableCell className="font-medium">{item.business_date}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(item.total_bet_minor, displayCurrency)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(item.total_payout_minor, displayCurrency)}
|
||||
</TableCell>
|
||||
<TableCell className={signedProfitCell(item.approx_house_gross_minor, displayCurrency)}>
|
||||
{formatPlainMoney(item.approx_house_gross_minor, displayCurrency)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
case "player_win_loss":
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("preview.columns.playerWinLoss.primary")}</TableHead>
|
||||
<TableHead>{t("preview.columns.playerWinLoss.secondary")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.playerWinLoss.metricA")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.playerWinLoss.metricB")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.playerWinLoss.metricC")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{result.raw.map((item) => (
|
||||
<TableRow key={item.player_id}>
|
||||
<TableCell className="font-medium">{item.username}</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{adminAgentDisplayLabel(item)}
|
||||
<span className="mt-0.5 block text-muted-foreground">ID {item.player_id}</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(item.total_bet_minor, displayCurrency)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(item.total_payout_minor, displayCurrency)}
|
||||
</TableCell>
|
||||
<TableCell className={signedProfitCell(item.net_win_loss_minor, displayCurrency)}>
|
||||
{formatPlainMoney(item.net_win_loss_minor, displayCurrency)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
case "play_dimension":
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("preview.columns.playDimension.primary")}</TableHead>
|
||||
<TableHead>{t("preview.columns.playDimension.secondary")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.playDimension.metricA")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.playDimension.metricB")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.playDimension.metricC")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{result.raw.map((item) => (
|
||||
<TableRow key={`${item.play_code}-${item.dimension}`}>
|
||||
<TableCell className="font-medium">{playCodeLabel(item.play_code)}</TableCell>
|
||||
<TableCell>{item.dimension}D</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(item.total_bet_minor, displayCurrency)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(item.total_payout_minor, displayCurrency)}
|
||||
</TableCell>
|
||||
<TableCell className={signedProfitCell(item.approx_house_gross_minor, displayCurrency)}>
|
||||
{formatPlainMoney(item.approx_house_gross_minor, displayCurrency)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
case "player_transfer":
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("preview.columns.playerTransfer.primary")}</TableHead>
|
||||
<TableHead>{t("preview.columns.playerTransfer.secondary")}</TableHead>
|
||||
<TableHead>{t("preview.columns.playerTransfer.metricA")}</TableHead>
|
||||
<TableHead>{t("preview.columns.playerTransfer.metricB")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.playerTransfer.metricC")}</TableHead>
|
||||
<TableHead>{t("preview.columns.playerTransfer.status")}</TableHead>
|
||||
<TableHead>{t("preview.columns.playerTransfer.time")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{result.raw.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="font-mono text-xs">{item.transfer_no}</TableCell>
|
||||
<TableCell>{optionText(item.username, item.nickname) || item.player_id}</TableCell>
|
||||
<TableCell>{item.direction}</TableCell>
|
||||
<TableCell>
|
||||
<AdminStatusBadge status={item.status}>{item.status}</AdminStatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{item.currency_code} {item.amount}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[12rem] truncate">{item.external_ref_no || "—"}</TableCell>
|
||||
<TableCell>{formatTs(item.created_at)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
case "hot_number_risk":
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("preview.columns.hotNumberRisk.primary")}</TableHead>
|
||||
<TableHead>{t("preview.columns.hotNumberRisk.secondary")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.hotNumberRisk.metricA")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.hotNumberRisk.metricB")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.hotNumberRisk.metricC")}</TableHead>
|
||||
<TableHead>{t("preview.columns.hotNumberRisk.status")}</TableHead>
|
||||
<TableHead>{t("preview.columns.hotNumberRisk.extra")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">{result.raw.pool.normalized_number}</TableCell>
|
||||
<TableCell>{result.raw.draw_no}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(result.raw.pool.total_cap_amount, result.raw.currency_code)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(result.raw.pool.locked_amount, result.raw.currency_code)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(result.raw.pool.remaining_amount, result.raw.currency_code)}
|
||||
</TableCell>
|
||||
<TableCell>{result.raw.pool.is_sold_out ? t("yes") : t("no")}</TableCell>
|
||||
<TableCell>{formatUsagePercent(result.raw.pool.usage_ratio)}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
{result.raw.logs.items.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("preview.sections.lockLogs")}</TableHead>
|
||||
<TableHead>{t("preview.columns.hotNumberRisk.secondary")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.hotNumberRisk.metricA")}</TableHead>
|
||||
<TableHead>{t("preview.columns.hotNumberRisk.metricB")}</TableHead>
|
||||
<TableHead>{t("preview.columns.hotNumberRisk.metricC")}</TableHead>
|
||||
<TableHead>{t("preview.columns.hotNumberRisk.time")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{result.raw.logs.items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="font-mono text-xs">#{item.id}</TableCell>
|
||||
<TableCell>{item.action_type}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(item.amount, result.raw.currency_code)}
|
||||
</TableCell>
|
||||
<TableCell>{playCodeLabel(item.play_code)}</TableCell>
|
||||
<TableCell>{item.ticket_no || "—"}</TableCell>
|
||||
<TableCell>{formatTs(item.created_at)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
case "sold_out_number":
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("preview.columns.soldOut.primary")}</TableHead>
|
||||
<TableHead>{t("preview.columns.soldOut.secondary")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.soldOut.metricA")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.soldOut.metricB")}</TableHead>
|
||||
<TableHead className="text-right">{t("preview.columns.soldOut.metricC")}</TableHead>
|
||||
<TableHead>{t("preview.columns.soldOut.status")}</TableHead>
|
||||
<TableHead>{t("preview.columns.soldOut.extra")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{result.raw.map((item) => (
|
||||
<TableRow key={item.normalized_number}>
|
||||
<TableCell className="font-medium">{item.normalized_number}</TableCell>
|
||||
<TableCell>{drawNoLabel}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(item.total_cap_amount, displayCurrency)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(item.locked_amount, displayCurrency)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatPlainMoney(item.remaining_amount, displayCurrency)}
|
||||
</TableCell>
|
||||
<TableCell>{item.is_sold_out ? t("yes") : t("no")}</TableCell>
|
||||
<TableCell>{formatUsagePercent(item.usage_ratio)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
case "admin_audit":
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("preview.columns.adminAudit.primary")}</TableHead>
|
||||
<TableHead>{t("preview.columns.adminAudit.secondary")}</TableHead>
|
||||
<TableHead>{t("preview.columns.adminAudit.metricA")}</TableHead>
|
||||
<TableHead>{t("preview.columns.adminAudit.metricB")}</TableHead>
|
||||
<TableHead>{t("preview.columns.adminAudit.metricC")}</TableHead>
|
||||
<TableHead>{t("preview.columns.adminAudit.status")}</TableHead>
|
||||
<TableHead>{t("preview.columns.adminAudit.time")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{result.raw.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="font-mono text-xs">#{item.id}</TableCell>
|
||||
<TableCell>{item.operator_type}</TableCell>
|
||||
<TableCell>{item.operator_id}</TableCell>
|
||||
<TableCell>{item.module_code}</TableCell>
|
||||
<TableCell>{item.action_code}</TableCell>
|
||||
<TableCell>{item.target_type || "—"}</TableCell>
|
||||
<TableCell>{formatTs(item.created_at)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
default:
|
||||
return <AdminLoadingInline />;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user