import AppsOutlined from "@mui/icons-material/AppsOutlined"; import CardGiftcardOutlined from "@mui/icons-material/CardGiftcardOutlined"; import CasinoOutlined from "@mui/icons-material/CasinoOutlined"; import SavingsOutlined from "@mui/icons-material/SavingsOutlined"; import { useMemo, useState } from "react"; import { AdminFilterSelect, AdminListToolbar } from "@/shared/ui/AdminListLayout.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; import { KpiCard } from "@/shared/ui/KpiCard.jsx"; import { TimeText } from "@/shared/ui/TimeText.jsx"; import { luckyGiftPoolKey } from "../api.js"; import { formatNumber, formatPercentFromPPM } from "../format.js"; import { LuckyGiftPoolActions } from "./LuckyGiftPoolActions.jsx"; import { OpsStatusBadge } from "./OpsStatusBadge.jsx"; export function OverviewView({ canCredit, canDebit, data, disabled, onCredit, onDebit }) { const { appSummaries, poolBalances, summary } = data; const [poolScope, setPoolScope] = useState("current"); const poolBalanceColumns = useMemo( () => buildPoolBalanceColumns({ canCredit, canDebit, disabled, onCredit, onDebit }), [canCredit, canDebit, disabled, onCredit, onDebit], ); const poolCounts = useMemo( () => ({ all: poolBalances.length, current: poolBalances.filter((pool) => !pool.is_historical).length, history: poolBalances.filter((pool) => pool.is_historical).length, }), [poolBalances], ); const visiblePoolBalances = useMemo(() => { if (poolScope === "history") { return poolBalances.filter((pool) => pool.is_historical); } if (poolScope === "all") { return poolBalances; } // 当前策略是默认视图;历史账本只从显式筛选进入,避免同名 V2/V3 被误读为重复奖池。 return poolBalances.filter((pool) => !pool.is_historical); }, [poolBalances, poolScope]); return (
} /> item.app_code} title="各应用抽奖汇总" total={appSummaries.length} />
); } function buildPoolBalanceColumns({ canCredit, canDebit, disabled, onCredit, onDebit }) { const columns = [ { key: "app_code", label: "应用", width: "minmax(96px, 0.8fr)" }, { key: "pool_id", label: "奖池", width: "minmax(110px, 1fr)" }, { key: "strategy_version", label: "策略", width: "minmax(112px, 0.9fr)" }, { key: "materialized", label: "水位状态", // materialized=false 是 owner 根据当前策略推导的初始水位;仍允许补水或扣水,首次调整由 owner 原子物化该策略奖池。 render: (item) => { if (item.is_historical) { return ; } return item.materialized === false ? ( ) : ( ); }, width: "minmax(132px, 0.9fr)", }, { key: "balance", label: "当前余额", render: (item) => formatNumber(item.balance) }, { key: "available_balance", label: "可用余额", render: (item) => formatNumber(item.available_balance) }, { key: "reserve_floor", label: "保底线", render: (item) => formatNumber(item.reserve_floor), width: "minmax(96px, 0.8fr)", }, { key: "total_in", label: "业务流入", render: (item) => formatNumber(item.total_in) }, { key: "total_out", label: "返奖流出", render: (item) => formatNumber(item.total_out) }, { key: "manual_credit_total", label: "人工添加", render: (item) => formatNumber(item.manual_credit_total) }, { key: "manual_debit_total", label: "人工扣减", render: (item) => formatNumber(item.manual_debit_total) }, { key: "updated_at_ms", label: "更新时间", render: (item) => (item.materialized === false ? "-" : ), width: "minmax(150px, 1fr)", }, ]; if (canCredit || canDebit) { columns.push({ fixed: "right", key: "actions", label: "操作", render: (item) => ( ), width: canCredit && canDebit ? "220px" : "120px", }); } return columns; } const appSummaryColumns = [ { key: "app_code", label: "应用", width: "minmax(96px, 0.8fr)" }, { key: "total_draws", label: "抽奖次数", render: (item) => formatNumber(item.total_draws) }, { key: "unique_users", label: "参与用户", render: (item) => formatNumber(item.unique_users) }, { key: "unique_rooms", label: "参与房间", render: (item) => formatNumber(item.unique_rooms) }, { key: "total_spent_coins", label: "消耗金币", render: (item) => formatNumber(item.total_spent_coins) }, { key: "total_reward_coins", label: "返还金币", render: (item) => formatNumber(item.total_reward_coins) }, { key: "actual_rtp_ppm", label: "实际 RTP", render: (item) => (item.total_draws > 0 ? formatPercentFromPPM(item.actual_rtp_ppm) : "-"), }, { key: "exceptions", label: "发放异常", // 待发放和失败合并成一列告警:正常应为 0,非 0 说明发奖链路有积压或失败,需要跟进抽奖记录。 render: (item) => { const pending = Number(item.pending_draws || 0); const failed = Number(item.failed_draws || 0); if (!pending && !failed) { return ; } return ( ); }, width: "minmax(170px, 1.2fr)", }, ]; function countBySource(apps = [], source) { return apps.filter((item) => (item.source || "registry") === source).length; }