样式
This commit is contained in:
parent
5400f1dd57
commit
ac5d3a53d9
@ -64,7 +64,11 @@ export function SelfGameScreen({ gameId = "all", loading = false, onGameChange,
|
||||
|
||||
<section className="self-game-panel-grid">
|
||||
<ParticipantTable items={model.participants} loading={showSkeleton} />
|
||||
<WinLossTable summary={model.winLoss} loading={showSkeleton} />
|
||||
<RiskTable items={model.risks} loading={showSkeleton} />
|
||||
</section>
|
||||
|
||||
<section className="self-game-panel-grid self-game-panel-grid--single">
|
||||
<MetricDefinitionList items={model.definitions} loading={showSkeleton} />
|
||||
</section>
|
||||
</section>
|
||||
@ -76,32 +80,47 @@ function buildSelfGameModel(overview) {
|
||||
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 humanNetWin = participantNetWin(participants, "human", numberValue(totals.payout_coin) + numberValue(totals.refund_coin) - numberValue(totals.user_stake_coin));
|
||||
const robotNetWin = participantNetWin(participants, "robot", numberValue(totals.robot_payout_coin) - numberValue(totals.robot_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 = {
|
||||
drawCount: numberValue(totals.draw_count),
|
||||
humanNetWin,
|
||||
loserCount: numberValue(totals.loser_count),
|
||||
robotNetWin,
|
||||
winnerCount: numberValue(totals.winner_count)
|
||||
};
|
||||
|
||||
return {
|
||||
kpis: [
|
||||
metric("H5 PV / UV", `${formatNumber(pageOpenEvent.event_count)} / ${formatNumber(pageOpenEvent.user_count)}`, "页面打开人数和次数"),
|
||||
metric("点击匹配 UV", formatNumber(matchClickEvent.user_count), `${formatNumber(matchClickEvent.event_count)} 次点击`),
|
||||
metric("匹配成功 UV", formatNumber(matchSuccessEvent.user_count), `${formatPercent(overview?.conversion?.match_click_to_success_rate)} 点击后成功`),
|
||||
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.created_matches), `成功 ${formatNumber(totals.matched_matches)} / 结算 ${formatNumber(totals.settled_matches)}`),
|
||||
metric("取消 / 失败", `${formatNumber(totals.canceled_matches)} / ${formatNumber(totals.failed_matches)}`, `${formatPercent(totals.cancel_rate)} / ${formatPercent(totals.fail_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(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), `强制结果 ${formatNumber(totals.forced_player_lose_matches)} 次`),
|
||||
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.humanNetWin)}`),
|
||||
metric("次日 / 7日留存", `${formatPercent(retention.day1_rate)} / ${formatPercent(retention.day7_rate)}`, `样本 ${formatNumber(retention.cohort_users)} 人`)
|
||||
],
|
||||
definitions: overview?.metric_definitions || [],
|
||||
funnel: overview?.funnel || [],
|
||||
participants: overview?.participant_distribution || [],
|
||||
pools: overview?.pool_stats || [],
|
||||
participants,
|
||||
pools,
|
||||
risks: overview?.user_risk || [],
|
||||
stakes: overview?.stake_breakdown || []
|
||||
stakes: overview?.stake_breakdown || [],
|
||||
winLoss
|
||||
};
|
||||
}
|
||||
|
||||
@ -154,10 +173,16 @@ function StakeTable({ items, loading }) {
|
||||
emptyText="暂无档位数据"
|
||||
loading={loading}
|
||||
rows={items}
|
||||
wide
|
||||
columns={[
|
||||
{ key: "stake_coin", label: "档位", render: (row) => 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: "stake", label: "下注", render: (row) => formatCoin(numberValue(row.user_stake_coin) + numberValue(row.robot_stake_coin)) },
|
||||
{ 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) }
|
||||
]}
|
||||
/>
|
||||
@ -195,6 +220,7 @@ function ParticipantTable({ items, loading }) {
|
||||
{ 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) }
|
||||
]}
|
||||
/>
|
||||
@ -202,6 +228,30 @@ function ParticipantTable({ items, loading }) {
|
||||
);
|
||||
}
|
||||
|
||||
function WinLossTable({ loading, summary }) {
|
||||
const rows = [
|
||||
{ metric: "赢家数", value: summary.winnerCount, note: "按参与人结果统计" },
|
||||
{ metric: "输家数", value: summary.loserCount, note: "按参与人结果统计" },
|
||||
{ metric: "平局数", value: summary.drawCount, note: "按参与人结果统计" },
|
||||
{ metric: "真人净输赢", value: summary.humanNetWin, note: "真人派奖加退款减真人下注", type: "coin" },
|
||||
{ metric: "机器人净输赢", value: summary.robotNetWin, note: "机器人派奖减机器人下注", type: "coin" }
|
||||
];
|
||||
return (
|
||||
<Panel className="panel--self-game" title="用户输赢">
|
||||
<DataTable
|
||||
emptyText="暂无输赢数据"
|
||||
loading={loading}
|
||||
rows={rows}
|
||||
columns={[
|
||||
{ key: "metric", label: "指标", render: (row) => row.metric },
|
||||
{ key: "value", label: "数值", render: (row) => (row.type === "coin" ? formatCoin(row.value) : formatNumber(row.value)) },
|
||||
{ key: "note", label: "口径", render: (row) => row.note }
|
||||
]}
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function RiskTable({ items, loading }) {
|
||||
return (
|
||||
<Panel className="panel--self-game" title="高风险用户">
|
||||
@ -240,7 +290,7 @@ function MetricDefinitionList({ items, loading }) {
|
||||
);
|
||||
}
|
||||
|
||||
function DataTable({ columns, emptyText, loading, rows }) {
|
||||
function DataTable({ columns, emptyText, loading, rows, wide = false }) {
|
||||
if (loading) {
|
||||
return <div className="panel-empty">加载中...</div>;
|
||||
}
|
||||
@ -249,7 +299,7 @@ function DataTable({ columns, emptyText, loading, rows }) {
|
||||
}
|
||||
return (
|
||||
<div className="self-game-table-wrap">
|
||||
<table className="self-game-table">
|
||||
<table className={["self-game-table", wide ? "self-game-table--wide" : ""].filter(Boolean).join(" ")}>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
@ -272,11 +322,29 @@ function DataTable({ columns, emptyText, loading, rows }) {
|
||||
}
|
||||
|
||||
function rowKey(row, rowIndex) {
|
||||
return [row.game_id, row.user_id, row.stake_coin, row.event_name, row.reason, row.participant_type, row.result, row.rps_gesture, row.dice_point, 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 participantNetWin(items, participantType, fallback) {
|
||||
const matched = items.filter((item) => item.participant_type === participantType);
|
||||
if (!matched.length) {
|
||||
return fallback;
|
||||
}
|
||||
return matched.reduce((total, item) => total + numberValue(item.net_win_coin), 0);
|
||||
}
|
||||
|
||||
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 "入池";
|
||||
|
||||
@ -835,7 +835,7 @@
|
||||
|
||||
.self-game-screen {
|
||||
display: grid;
|
||||
grid-template-rows: 44px 220px repeat(2, minmax(236px, auto));
|
||||
grid-template-rows: 44px auto repeat(3, minmax(236px, auto));
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
}
|
||||
@ -871,6 +871,19 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.self-game-kpi-grid .metric-card {
|
||||
grid-column: span 1;
|
||||
height: 118px;
|
||||
}
|
||||
|
||||
.self-game-kpi-grid .metric-content {
|
||||
grid-template-rows: auto minmax(0, 1fr) auto 18px;
|
||||
}
|
||||
|
||||
.self-game-kpi-grid .metric-content strong {
|
||||
font-size: clamp(16px, 0.98vw, 21px);
|
||||
}
|
||||
|
||||
.self-game-panel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@ -878,6 +891,14 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.self-game-panel-grid--single {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.self-game-panel-grid .databi-panel {
|
||||
min-height: 236px;
|
||||
}
|
||||
|
||||
.self-game-panel-grid--single .databi-panel {
|
||||
min-height: 190px;
|
||||
}
|
||||
|
||||
@ -144,6 +144,10 @@
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.self-game-table--wide {
|
||||
min-width: 980px;
|
||||
}
|
||||
|
||||
.self-game-table th,
|
||||
.self-game-table td {
|
||||
height: 30px;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user