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) => ( ))} {rows.map((row, rowIndex) => ( {columns.map((column) => ( ))} ))}
{column.label}
{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 "--"; }