208 lines
9.2 KiB
JavaScript
208 lines
9.2 KiB
JavaScript
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 (
|
||
<div className="ops-view">
|
||
<div className="kpi-grid ops-kpi-grid">
|
||
<KpiCard
|
||
icon={AppsOutlined}
|
||
label="运营应用"
|
||
sub={`主数据 ${formatNumber(countBySource(data.apps, "registry"))} · 外部接入 ${formatNumber(countBySource(data.apps, "fixed"))}`}
|
||
value={formatNumber(summary.activeApps)}
|
||
/>
|
||
<KpiCard
|
||
icon={CardGiftcardOutlined}
|
||
label="已发布奖池配置"
|
||
sub={`启用 ${formatNumber(summary.enabledPools)} · 停用 ${formatNumber(summary.configuredPools - summary.enabledPools)}`}
|
||
tone="info"
|
||
value={formatNumber(summary.configuredPools)}
|
||
/>
|
||
<KpiCard
|
||
icon={CasinoOutlined}
|
||
label="抽奖次数"
|
||
sub={`成功 ${formatNumber(summary.grantedDraws)} · 待发放 ${formatNumber(summary.pendingDraws)} · 失败 ${formatNumber(summary.failedDraws)}`}
|
||
tone="warning"
|
||
value={formatNumber(summary.totalDraws)}
|
||
/>
|
||
<KpiCard
|
||
icon={SavingsOutlined}
|
||
label="当前奖池金币"
|
||
sub={`可用 ${formatNumber(summary.poolAvailableTotal)} · 保底 ${formatNumber(summary.poolReserveTotal)}`}
|
||
tone="success"
|
||
value={formatNumber(summary.poolBalanceTotal)}
|
||
/>
|
||
</div>
|
||
|
||
<AdminListToolbar
|
||
filters={
|
||
<AdminFilterSelect
|
||
label="奖池范围"
|
||
options={[
|
||
["current", `当前奖池 (${poolCounts.current})`],
|
||
["history", `历史奖池 (${poolCounts.history})`],
|
||
["all", `全部奖池 (${poolCounts.all})`],
|
||
]}
|
||
value={poolScope}
|
||
onChange={setPoolScope}
|
||
/>
|
||
}
|
||
/>
|
||
<DataTable
|
||
columns={poolBalanceColumns}
|
||
emptyLabel={poolScope === "history" ? "暂无历史奖池" : "暂无奖池余额"}
|
||
items={visiblePoolBalances}
|
||
minWidth="1480px"
|
||
rowKey={luckyGiftPoolKey}
|
||
title="奖池实时水位"
|
||
total={visiblePoolBalances.length}
|
||
/>
|
||
|
||
<DataTable
|
||
columns={appSummaryColumns}
|
||
emptyLabel="暂无抽奖数据"
|
||
items={appSummaries}
|
||
minWidth="920px"
|
||
rowKey={(item) => item.app_code}
|
||
title="各应用抽奖汇总"
|
||
total={appSummaries.length}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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 <OpsStatusBadge label="历史奖池" tone="stopped" />;
|
||
}
|
||
return item.materialized === false ? (
|
||
<OpsStatusBadge label="默认水位" />
|
||
) : (
|
||
<OpsStatusBadge label="已入账" tone="succeeded" />
|
||
);
|
||
},
|
||
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 ? "-" : <TimeText value={item.updated_at_ms} />),
|
||
width: "minmax(150px, 1fr)",
|
||
},
|
||
];
|
||
|
||
if (canCredit || canDebit) {
|
||
columns.push({
|
||
fixed: "right",
|
||
key: "actions",
|
||
label: "操作",
|
||
render: (item) => (
|
||
<LuckyGiftPoolActions
|
||
canCredit={canCredit}
|
||
canDebit={canDebit}
|
||
disabled={disabled}
|
||
pool={item}
|
||
onCredit={onCredit}
|
||
onDebit={onDebit}
|
||
/>
|
||
),
|
||
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 <OpsStatusBadge label="正常" tone="succeeded" />;
|
||
}
|
||
return (
|
||
<OpsStatusBadge
|
||
label={`待发放 ${formatNumber(pending)} · 失败 ${formatNumber(failed)}`}
|
||
tone={failed ? "danger" : "warning"}
|
||
/>
|
||
);
|
||
},
|
||
width: "minmax(170px, 1.2fr)",
|
||
},
|
||
];
|
||
|
||
function countBySource(apps = [], source) {
|
||
return apps.filter((item) => (item.source || "registry") === source).length;
|
||
}
|