import { MetricCard } from "./MetricCard.jsx";
import { Panel } from "./Panel.jsx";
import { formatCoin, formatNumber, formatPercent } from "../utils/format.js";
import { numberValue } from "../utils/number.js";
const gameOptions = [
{ label: "全部", value: "all" },
{ label: "Dice", value: "dice" },
{ label: "Rock", value: "rock" }
];
const eventLabels = {
page_open: "页面打开",
config_load_success: "配置成功",
config_load_failed: "配置失败",
gesture_select: "选择手势",
match_api: "匹配接口",
match_cancel: "取消匹配",
stake_selected: "选择档位",
stake_select: "选择档位",
match_click: "点击匹配",
match_success: "匹配成功",
animation_start: "进入开奖",
preload_complete: "预加载完成",
reveal_enter: "进入开奖",
roll_api: "开奖接口",
settlement_show: "结算展示",
play_again: "再来一局",
play_again_click: "再来一局",
exit_click: "退出"
};
export function SelfGameScreen({ gameId = "all", loading = false, onGameChange, overview }) {
const model = buildSelfGameModel(overview);
const showSkeleton = loading && !overview;
return (
自研游戏
Dice / Rock H5 聚合统计
{gameOptions.map((item) => (
))}
{model.kpis.map((item) => (
))}
);
}
function buildSelfGameModel(overview) {
const events = eventIndex(overview?.events);
const totals = overview?.match_totals || {};
const retention = overview?.retention || {};
const totalStake = numberValue(totals.user_stake_coin) + numberValue(totals.robot_stake_coin);
const participants = Array.isArray(overview?.participant_distribution) ? overview.participant_distribution : [];
const pools = Array.isArray(overview?.pool_stats) ? overview.pool_stats : [];
const userResult = realUserResultCounts(participants, totals);
const userNetWin = numberValue(totals.payout_coin) + numberValue(totals.refund_coin) - numberValue(totals.user_stake_coin);
const poolBalance = latestPoolBalance(pools);
const settlementEvent = events.get("settlement_show");
const matchClickEvent = events.get("match_click");
const matchSuccessEvent = events.get("match_success");
const pageOpenEvent = events.get("page_open");
const winLoss = {
countNote: userResult.countNote,
drawCount: userResult.draw,
loserCount: userResult.lose,
payoutCoin: numberValue(totals.payout_coin),
refundCoin: numberValue(totals.refund_coin),
userNetWin,
userStakeCoin: numberValue(totals.user_stake_coin),
winnerCount: userResult.win
};
return {
kpis: [
metric("游戏 PV / UV", `${formatNumber(pageOpenEvent.event_count)} / ${formatNumber(pageOpenEvent.user_count)}`, "进入 dice、rock H5 的次数和人数"),
metric("开局人数", `点击 ${formatNumber(matchClickEvent.user_count)} / 成功 ${formatNumber(matchSuccessEvent.user_count)}`, `${formatNumber(matchClickEvent.event_count)} 次点击`),
metric("创建局", formatNumber(totals.created_matches), "服务端创建的对局数"),
metric("匹配成功局", formatNumber(totals.matched_matches), `${formatPercent(overview?.conversion?.match_click_to_success_rate)} 点击后成功`),
metric("结算完成 UV", formatNumber(settlementEvent.user_count), `${formatPercent(overview?.conversion?.page_to_settlement_rate)} 页面到结算`),
metric("已结算局", formatNumber(totals.settled_matches), "已经完成派奖或退款的局"),
metric("取消 / 失败局", `${formatNumber(totals.canceled_matches)} / ${formatNumber(totals.failed_matches)}`, `${formatPercent(totals.cancel_rate)} / ${formatPercent(totals.fail_rate)}`),
metric("参与人次", formatNumber(numberValue(totals.human_participants) + numberValue(totals.robot_participants)), `真人 ${formatNumber(totals.human_participants)} / 机器人 ${formatNumber(totals.robot_participants)}`),
metric("总下注金币", formatCoin(totalStake), `真人 ${formatCoin(totals.user_stake_coin)} / 机器人 ${formatCoin(totals.robot_stake_coin)}`),
metric("派奖金额", formatCoin(totals.payout_coin), `退款 ${formatCoin(totals.refund_coin)}`),
metric("平台抽水", formatCoin(totals.platform_profit_coin), formatPercent(totals.platform_profit_rate)),
metric("奖池变化", formatCoin(totals.pool_delta_coin), `余额 ${formatCoin(poolBalance)} / 强制 ${formatNumber(totals.forced_player_lose_matches)} 次`),
metric("用户输赢", `${formatNumber(winLoss.winnerCount)} / ${formatNumber(winLoss.loserCount)} / ${formatNumber(winLoss.drawCount)}`, `真实用户净输赢 ${formatCoin(winLoss.userNetWin)}`),
metric("次日 / 7日留存", `${formatPercent(retention.day1_rate)} / ${formatPercent(retention.day7_rate)}`, `样本 ${formatNumber(retention.cohort_users)} 人`)
],
definitions: overview?.metric_definitions || [],
funnel: overview?.funnel || [],
participants,
pools,
risks: overview?.user_risk || [],
stakes: overview?.stake_breakdown || [],
winLoss
};
}
function eventIndex(items) {
const index = new Map();
(Array.isArray(items) ? items : []).forEach((item) => {
const name = item.event_name || "";
const current = index.get(name) || emptyEvent();
index.set(name, {
event_count: numberValue(current.event_count) + numberValue(item.event_count),
user_count: numberValue(current.user_count) + numberValue(item.user_count)
});
});
return {
get(name) {
return index.get(name) || emptyEvent();
}
};
}
function emptyEvent() {
return { event_count: 0, user_count: 0 };
}
function metric(label, value, caption) {
return { caption, label, value };
}
function FunnelTable({ funnel, loading }) {
return (
eventLabels[row.event_name] || row.event_name || "--" },
{ key: "user_count", label: "UV", render: (row) => formatNumber(row.user_count) },
{ key: "event_count", label: "次数", render: (row) => formatNumber(row.event_count) }
]}
/>
);
}
function StakeTable({ items, loading }) {
return (
formatCoin(row.stake_coin) },
{ key: "created_matches", label: "创建局", render: (row) => formatNumber(row.created_matches) },
{ key: "matched_matches", label: "匹配局", render: (row) => formatNumber(row.matched_matches) },
{ key: "settled_matches", label: "结算局", render: (row) => formatNumber(row.settled_matches) },
{ key: "user_stake_coin", label: "真人下注", render: (row) => formatCoin(row.user_stake_coin) },
{ key: "robot_stake_coin", label: "机器人下注", render: (row) => formatCoin(row.robot_stake_coin) },
{ key: "stake", label: "总下注", render: (row) => formatCoin(numberValue(row.user_stake_coin) + numberValue(row.robot_stake_coin)) },
{ key: "payout_coin", label: "派奖", render: (row) => formatCoin(row.payout_coin) },
{ key: "profit", label: "盈利", render: (row) => formatCoin(row.platform_profit_coin) }
]}
/>
);
}
function PoolTable({ items, loading }) {
return (
directionLabel(row.direction) },
{ key: "reason", label: "原因", render: (row) => row.reason || "--" },
{ key: "flow", label: "入 / 出", render: (row) => `${formatCoin(row.in_coin)} / ${formatCoin(row.out_coin)}` },
{ key: "balance_after_coin", label: "余额", render: (row) => formatCoin(row.balance_after_coin) }
]}
/>
);
}
function ParticipantTable({ items, loading }) {
return (
formatCoin(row.stake_coin) },
{ key: "type", label: "对象", render: (row) => participantLabel(row.participant_type) },
{ key: "play", label: "手势 / 点数", render: (row) => playLabel(row) },
{ key: "result", label: "结果", render: (row) => resultLabel(row.result) },
{ key: "count", label: "人次", render: (row) => formatNumber(row.participant_count) },
{ key: "net_win_coin", label: "净输赢", render: (row) => formatCoin(row.net_win_coin) },
{ key: "win_rate", label: "胜率", render: (row) => formatPercent(row.win_rate) }
]}
/>
);
}
function WinLossTable({ loading, summary }) {
const rows = [
{ metric: "真实用户赢家数", value: summary.winnerCount, note: summary.countNote },
{ metric: "真实用户输家数", value: summary.loserCount, note: summary.countNote },
{ metric: "真实用户平局数", value: summary.drawCount, note: summary.countNote },
{ metric: "真实用户下注", value: summary.userStakeCoin, note: "match_totals.user_stake_coin", type: "coin" },
{ metric: "真实用户派奖", value: summary.payoutCoin, note: "match_totals.payout_coin", type: "coin" },
{ metric: "真实用户退款", value: summary.refundCoin, note: "match_totals.refund_coin", type: "coin" },
{ metric: "真实用户净输赢", value: summary.userNetWin, note: "派奖 + 退款 - 真实用户下注", type: "coin" }
];
return (
row.metric },
{ key: "value", label: "数值", render: (row) => (row.type === "coin" ? formatCoin(row.value) : formatNumber(row.value)) },
{ key: "note", label: "口径", render: (row) => row.note }
]}
/>
);
}
function RiskTable({ items, loading }) {
return (
},
{ key: "match_count", label: "局数", render: (row) => formatNumber(row.match_count) },
{ key: "win_rate", label: "胜率", render: (row) => formatPercent(row.win_rate) },
{ key: "net_win_coin", label: "净赢", render: (row) => formatCoin(row.net_win_coin) },
{ key: "cancel_rate", label: "取消率", render: (row) => formatPercent(row.cancel_rate) }
]}
/>
);
}
function RiskUserCell({ row }) {
const nickname = row.nickname || row.username || "";
const shortID = row.short_id || row.display_user_id || "";
const userID = row.user_id ? String(row.user_id) : "";
const avatar = row.avatar || "";
const primary = nickname || shortID || userID || "--";
const secondary = shortID ? `ID ${shortID}` : userID ? `UID ${userID}` : "";
return (
{avatar ?

:
{primary.slice(0, 1).toUpperCase()}}
{primary}
{secondary ? {secondary} : null}
);
}
function MetricDefinitionList({ items, loading }) {
return (
{loading ? 加载中...
: null}
{!loading && !items.length ? 暂无口径说明
: null}
{!loading && items.length ? (
{items.map((item) => (
{item.metric}
{item.definition}
))}
) : null}
);
}
function DataTable({ columns, emptyText, loading, rows, wide = false }) {
if (loading) {
return 加载中...
;
}
if (!rows.length) {
return {emptyText}
;
}
return (
{columns.map((column) => (
| {column.label} |
))}
{rows.map((row, rowIndex) => (
{columns.map((column) => (
| {column.render(row)} |
))}
))}
);
}
function rowKey(row, rowIndex) {
return [row.game_id, row.user_id, row.metric, row.stake_coin, row.event_name, row.reason, row.participant_type, row.result, row.rps_gesture, row.dice_point, rowIndex]
.filter((item) => item !== undefined && item !== null && item !== "")
.join(":");
}
function realUserResultCounts(items, totals) {
const result = { countNote: "按 participant_distribution 中真实用户结果统计", draw: 0, lose: 0, win: 0 };
let userRows = 0;
items.forEach((item) => {
if (!isRealUserParticipant(item.participant_type)) {
return;
}
userRows += 1;
const count = numberValue(item.participant_count);
if (item.result === "win") {
result.win += count;
} else if (item.result === "lose") {
result.lose += count;
} else if (item.result === "draw") {
result.draw += count;
}
});
if (userRows > 0) {
return result;
}
return {
countNote: "缺少真实用户分布时回退 match_totals 参与人结果",
draw: numberValue(totals.draw_count),
lose: numberValue(totals.loser_count),
win: numberValue(totals.winner_count)
};
}
function isRealUserParticipant(value) {
return value === "user" || value === "human";
}
function latestPoolBalance(items) {
if (!items.length) {
return 0;
}
return items.reduce((balance, item) => {
const next = numberValue(item.balance_after_coin);
return next !== 0 ? next : balance;
}, 0);
}
function directionLabel(value) {
if (value === "in") {
return "入池";
}
if (value === "out") {
return "出池";
}
return value || "--";
}
function participantLabel(value) {
if (value === "robot") {
return "机器人";
}
if (isRealUserParticipant(value)) {
return "真人";
}
return value || "--";
}
function resultLabel(value) {
if (value === "win") {
return "胜";
}
if (value === "lose") {
return "负";
}
if (value === "draw") {
return "平";
}
return value || "--";
}
function playLabel(row) {
if (row.rps_gesture) {
return row.rps_gesture;
}
if (numberValue(row.dice_point) > 0) {
return `${row.dice_point} 点`;
}
return "--";
}