import { Fragment, useCallback, useEffect, useMemo, useState } from "react"; import { fetchPlatformGrantRecords, fetchPlatformGrantUsers } from "../api.js"; import { EChart } from "../charts/EChart.jsx"; import { createReportRevenueOption } from "../charts/options/createReportRevenueOption.js"; import { formatCoin, formatMoneyFull, formatPercent } from "../utils/format.js"; const textCollator = new Intl.Collator("zh-Hans", { numeric: true, sensitivity: "base" }); const reportTabs = [ { key: "country", label: "国家经营报表" }, { key: "revenue", label: "收入趋势" }, { key: "gift", label: "礼物消费" }, { key: "lucky", label: "幸运礼物" }, { key: "game", label: "游戏流水" }, { key: "robot", label: "机器人送礼" } ]; const platformGrantRecordSourceOptions = [ { label: "全部来源", value: "all" }, { label: "任务奖励", value: "WalletTaskRewardCredited" }, { label: "房间流水奖励", value: "WalletRoomTurnoverRewardCredited" }, { label: "邀请活动奖励", value: "WalletInviteActivityRewardCredited" }, { label: "代理开通奖励", value: "WalletAgencyOpeningRewardCredited" }, { label: "资源发放", value: "WalletBalanceChanged" }, { label: "平台发放", value: "WalletPlatformGrantCoinCredited" } ]; const countryColumns = [ { align: "left", key: "date_label", label: "日期", render: (row) => , sortable: false, stickyLeft: true }, { align: "left", key: "country", label: "国家", render: (row) => , sortable: false }, { key: "registrations", label: "注册", render: (row) => formatOptionalCoin(row.registrations), sortable: false }, { key: "active_users", label: "活跃用户", render: (row) => formatOptionalCoin(row.active_users), sortable: false }, { key: "paid_users", label: "付费用户", render: (row) => formatOptionalCoin(row.paid_users), sortable: false }, { key: "recharge_users", label: "充值用户", render: (row) => formatOptionalCoin(row.recharge_users), sortable: false }, { key: "recharge_usd_minor", label: "总充值", render: (row) => formatOptionalMoney(row.recharge_usd_minor), sortable: false }, { key: "new_user_recharge_usd_minor", label: "新用户充值", render: (row) => formatOptionalMoney(row.new_user_recharge_usd_minor), sortable: false }, { key: "coin_seller_recharge_usd_minor", label: "币商充值", render: (row) => formatOptionalMoney(row.coin_seller_recharge_usd_minor), sortable: false }, { key: "coin_seller_stock_coin", label: "币商充值金币", render: (row) => formatOptionalCoin(row.coin_seller_stock_coin), sortable: false }, { key: "coin_seller_transfer_coin", label: "币商出货金币", render: (row) => formatOptionalCoin(row.coin_seller_transfer_coin), sortable: false }, { key: "google_recharge_usd_minor", label: "谷歌充值", render: (row) => formatOptionalMoney(row.google_recharge_usd_minor), sortable: false }, { key: "mifapay_recharge_usd_minor", label: "三方充值", render: (row) => formatOptionalMoney(row.mifapay_recharge_usd_minor), sortable: false }, { key: "game_turnover", label: "游戏流水/利润率", render: (row) => , sortable: false }, { key: "lucky_gift_turnover", label: "幸运礼物流水/利润率", render: (row) => , sortable: false }, { key: "super_lucky_gift_turnover", label: "超级幸运礼物流水/利润率", render: (row) => , sortable: false }, { key: "gift_coin_spent", label: "礼物流水", render: (row) => formatOptionalCoin(row.gift_coin_spent), sortable: false }, { key: "coin_total", label: "用户金币数", render: (row) => formatOptionalCoin(row.coin_total), sortable: false }, { key: "consumed_coin", label: "消耗金币", render: (row) => formatOptionalCoin(row.consumed_coin), sortable: false }, { key: "output_coin", label: "产出金币", render: (row) => formatOptionalCoin(row.output_coin), sortable: false }, { key: "consume_output_ratio", label: "消耗产出比", render: (row) => , sortable: false }, { key: "platform_grant_coin", label: "平台发放金币", render: (row, context) => , sortable: false }, { key: "manual_grant_coin", label: "人工发放金币", render: (row) => formatOptionalCoin(row.manual_grant_coin), sortable: false }, { key: "salary_usd_minor", label: "工资总和(主播/代理)", render: (row) => formatOptionalMoney(row.salary_usd_minor), sortable: false }, { key: "salary_transfer_coin", label: "工资兑换金币", render: (row) => formatOptionalCoin(row.salary_transfer_coin), sortable: false }, { key: "avg_mic_online_ms", label: "平均麦上时间", render: (row) => formatDuration(row.avg_mic_online_ms), sortable: false }, { key: "mic_online_ms", label: "麦上总时长", render: (row) => formatDuration(row.mic_online_ms), sortable: false }, { key: "arpu_usd_minor", label: "ARPU", render: (row) => formatOptionalMoney(row.arpu_usd_minor), sortable: false }, { key: "arppu_usd_minor", label: "ARPPU", render: (row) => formatOptionalMoney(row.arppu_usd_minor), sortable: false }, { key: "payer_rate", label: "付费转化率", render: (row) => formatOptionalPercent(row.payer_rate), sortable: false }, { key: "recharge_conversion_rate", label: "充值转化率", render: (row) => formatOptionalPercent(row.recharge_conversion_rate), sortable: false } ]; const countryDetailColumns = countryColumns.filter((column) => column.key !== "date_label"); const revenueColumns = [ { align: "left", key: "label", label: "日期", render: (row) => row.label, sortValue: (row) => row.label, type: "text" }, { key: "google", label: "Google", render: (row) => formatMoneyFull(row.google) }, { key: "mifapay", label: "MifaPay", render: (row) => formatMoneyFull(row.mifapay) }, { key: "coin_seller", label: "币商", render: (row) => formatMoneyFull(row.coin_seller) }, { key: "total", label: "渠道合计", render: (row) => formatMoneyFull(row.total) }, { key: "lineTotal", label: "总计", render: (row) => formatMoneyFull(row.lineTotal ?? row.total) } ]; const giftColumns = [ { align: "left", key: "name", label: "礼物", render: (row) => row.name, sortValue: (row) => row.name, type: "text" }, { key: "value", label: "金币消费", render: (row) => formatCoin(row.value) }, { key: "share", label: "占比", render: (row) => formatPercent(row.share) }, { key: "trend_rate", label: "近 7 日", render: (row) => } ]; const luckyColumns = [ { align: "left", key: "pool_id", label: "奖池", render: (row) => row.pool_id, sortValue: (row) => row.pool_id, type: "text" }, { key: "turnover", label: "流水", render: (row) => formatCoin(row.turnover) }, { key: "payout", label: "返奖", render: (row) => formatCoin(row.payout) }, { key: "profit", label: "利润", render: (row) => formatCoin(row.profit) }, { key: "payout_rate", label: "返奖率", render: (row) => formatPercent(row.payout_rate) }, { key: "profit_rate", label: "利润率", render: (row) => formatPercent(row.profit_rate) } ]; const gameColumns = [ { align: "left", key: "game_id", label: "游戏", render: (row) => row.game_label || row.game_id, sortValue: (row) => row.game_label || row.game_id, type: "text" }, { key: "turnover_coin", label: "流水 (金币)", render: (row) => formatCoin(row.turnover_coin) }, { key: "profit_rate", label: "利润率", render: (row) => formatPercent(row.profit_rate) }, { key: "share", label: "占比", render: (row) => formatPercent(row.share) } ]; const metricColumns = [ { align: "left", key: "label", label: "指标", render: (row) => row.label, sortValue: (row) => row.label, type: "text" }, { key: "value", label: "数值", render: (row) => row.value, sortable: false }, { key: "unit", label: "单位", render: (row) => row.unit || "--", sortable: false }, { key: "caption", label: "对比口径", render: (row) => row.caption || "--", sortable: false }, { key: "delta", label: "变化", render: (row) => , sortable: false } ]; export function ReportOverview({ grantDrilldownQuery, loading = false, model, onMetricTrend, onOpenLuckyGiftPools }) { const [activeTab, setActiveTab] = useState("country"); const summaryCards = useMemo(() => createSummaryCards(model), [model]); return (
{summaryCards.map((card) => ( ))}
{reportTabs.map((tab) => ( ))}
{activeTab === "country" ? : null} {activeTab === "revenue" ? : null} {activeTab === "gift" ? : null} {activeTab === "lucky" ? : null} {activeTab === "game" ? : null} {activeTab === "robot" ? : null}
); } function ReportMetricCard({ card, loading, onOpenTrend }) { const clickable = Boolean(card.trendMetric?.trend?.length && onOpenTrend); if (loading) { return (
); } return (
onOpenTrend(card.trendMetric) : undefined} onKeyDown={clickable ? (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onOpenTrend(card.trendMetric); } } : undefined} >
{card.title} {card.delta ? : null}
{card.value}
{card.details.filter((detail) => detail.label !== card.title).map((detail) => ( {detail.label} {detail.value} ))}
); } function CountryReport({ grantDrilldownQuery, loading, rows }) { const [expandedRowIds, setExpandedRowIds] = useState(() => new Set()); const [grantCountryModal, setGrantCountryModal] = useState(null); const [grantUsersModal, setGrantUsersModal] = useState(null); const [grantRecordsModal, setGrantRecordsModal] = useState(null); const toggleRow = useCallback((rowId) => { setExpandedRowIds((current) => { const next = new Set(current); if (next.has(rowId)) { next.delete(rowId); } else { next.add(rowId); } return next; }); }, []); const tableRows = useMemo(() => rows.map((row) => { const children = Array.isArray(row.children) ? row.children : []; if (row.row_type !== "date_total" || !children.length) { return row; } return { ...row, expanded: expandedRowIds.has(row.row_id), onToggle: () => toggleRow(row.row_id) }; }), [expandedRowIds, rows, toggleRow]); const openGrantUsers = useCallback((row) => { setGrantCountryModal(null); setGrantRecordsModal(null); setGrantUsersModal({ countryLabel: row.country || "国家", query: buildPlatformGrantQuery(grantDrilldownQuery, row) }); }, [grantDrilldownQuery]); const openGrantCountries = useCallback((row) => { const countryRows = platformGrantCountryRows(row, rows); if (!countryRows.length) { return; } setGrantRecordsModal(null); setGrantUsersModal(null); setGrantCountryModal({ rows: countryRows, title: row.row_type === "date_total" ? `${row.date_label} 平台发放金币` : "平台发放金币" }); }, [rows]); const grantContext = useMemo(() => ({ onOpenPlatformGrantCountries: openGrantCountries, onOpenPlatformGrantUsers: openGrantUsers }), [openGrantCountries, openGrantUsers]); return ( } rows={tableRows} wide /> {grantCountryModal ? ( setGrantCountryModal(null)} onOpenUsers={openGrantUsers} /> ) : null} {grantUsersModal ? ( setGrantUsersModal(null)} onOpenRecords={(userRow) => setGrantRecordsModal({ countryLabel: grantUsersModal.countryLabel, query: { ...grantUsersModal.query, userId: userRow.user_id }, user: userRow })} /> ) : null} {grantRecordsModal ? ( setGrantRecordsModal(null)} /> ) : null} ); } function RevenueReport({ loading, rows }) { return ( {loading ? (
) : rows.length ? ( ) : (
当前无数据
)}
); } function GiftReport({ loading, metrics, rows }) { const total = rows.reduce((sum, row) => sum + Number(row.value || 0), 0) || 1; const giftRows = rows.map((row) => ({ ...row, share: Number(row.value || 0) / total })); return ( item.label.includes("礼物") || item.label.includes("币商"))} /> ); } function LuckyReport({ loading, metrics, onOpenLuckyGiftPools, rows }) { return ( 查看奖池明细 )} > item.label.includes("幸运礼物"))} /> ); } function GameReport({ loading, metrics, rows }) { const total = rows.reduce((sum, row) => sum + Number(row.turnover_coin || 0), 0) || 1; const gameRows = rows.map((row) => ({ ...row, share: Number(row.turnover_coin || 0) / total })); return ( item.label.includes("游戏"))} /> ); } function RobotReport({ loading, rows }) { return ( ); } function ReportPanel({ action, children }) { return (
{action ?
{action}
: null} {children}
); } function ReportMetricTable({ loading, rows }) { return (
); } function ReportTable({ columns, context, defaultSort, loading, preserveOrder = false, renderExpandedRow, rows, wide = false }) { const sortableDefault = defaultSort || firstSortableColumn(columns); const [sortState, setSortState] = useState(sortableDefault); const sortedRows = useMemo(() => preserveOrder ? rows : sortRows(rows, sortState, columns), [columns, preserveOrder, rows, sortState]); const handleSort = (column) => { if (preserveOrder || column.sortable === false) { return; } setSortState((current) => ({ direction: current?.key === column.key && current.direction === "desc" ? "asc" : "desc", key: column.key, type: column.type || "number" })); }; return (
{columns.map((column) => { const isSortable = !preserveOrder && column.sortable !== false; const isActive = sortState?.key === column.key; const headerClassName = [ isSortable ? "is-sortable" : "", column.stickyLeft ? "is-sticky-left" : "" ].filter(Boolean).join(" "); return ( ); })} {loading ? : null} {!loading && !rows.length ? : null} {!loading && sortedRows.map((row, index) => { const key = reportRowKey(row, index); const rowProps = reportTableRowProps(row); const { className: rowClassName, ...interactiveRowProps } = rowProps; return ( {columns.map((column) => ( ))} {row.expanded && renderExpandedRow ? ( ) : null} ); })}
# {isSortable ? ( ) : ( column.label )}
{reportRowIndex(row, index)} {column.render ? column.render(row, context) : row[column.key]}
{renderExpandedRow(row)}
); } function CountryDetailTable({ context, dateLabel, rows }) { if (!rows.length) { return
当前无数据
; } return (
{countryDetailColumns.map((column) => ( ))} {rows.map((row, index) => ( {countryDetailColumns.map((column) => ( ))} ))}
#{column.label}
{index + 1}{column.render ? column.render(row, context) : row[column.key]}
); } function PlatformGrantCoinCell({ context, row }) { const value = Number(row.platform_grant_coin || 0); const clickable = value > 0 && (row.row_type === "country" ? context?.onOpenPlatformGrantUsers : context?.onOpenPlatformGrantCountries); if (!clickable) { return formatOptionalCoin(row.platform_grant_coin); } const label = row.row_type === "country" ? `查看 ${row.country || "国家"} 平台发放金币用户明细` : `查看 ${row.date_label || "全部"} 平台发放金币国家明细`; return ( ); } function PlatformGrantCountryModal({ modal, onClose, onOpenUsers }) { return (
{modal.rows.length ? modal.rows.map((row) => ( )) : ( )}
国家 平台发放金币
{formatOptionalCoin(row.platform_grant_coin)}
当前无数据
); } function PlatformGrantUsersModal({ modal, onClose, onOpenRecords }) { const pageSize = 20; const [page, setPage] = useState(1); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); useEffect(() => { let active = true; setLoading(true); setError(""); fetchPlatformGrantUsers({ ...modal.query, page, pageSize }) .then((nextData) => { if (active) { setData(nextData || {}); } }) .catch((err) => { if (active) { setError(err.message || "请求失败"); setData(null); } }) .finally(() => { if (active) { setLoading(false); } }); return () => { active = false; }; }, [modal.query, page]); const items = Array.isArray(data?.items) ? data.items : []; const total = Number(data?.total || 0); return (
{loading ? : null} {!loading && error ? : null} {!loading && !error && !items.length ? : null} {!loading && !error ? items.map((item) => { const openRecords = () => onOpenRecords(item); return ( { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openRecords(); } }} > ); }) : null}
用户 平台发放金币 记录数 最近发放时间
{error}
当前无数据
{item.avatar ? : null} {platformGrantUserLabel(item)} {platformGrantUserMeta(item)} {formatOptionalCoin(item.total_coin)} {formatOptionalCoin(item.record_count)} {formatDateTime(item.last_granted_at_ms)}
); } function PlatformGrantRecordsModal({ modal, onClose }) { const pageSize = 20; const [page, setPage] = useState(1); const [source, setSource] = useState("all"); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); useEffect(() => { let active = true; setLoading(true); setError(""); fetchPlatformGrantRecords({ ...modal.query, page, pageSize, source }) .then((nextData) => { if (active) { setData(nextData || {}); } }) .catch((err) => { if (active) { setError(err.message || "请求失败"); setData(null); } }) .finally(() => { if (active) { setLoading(false); } }); return () => { active = false; }; }, [modal.query, page, source]); const items = Array.isArray(data?.items) ? data.items : []; const total = Number(data?.total || 0); const selectSource = (event) => { setSource(event.target.value); setPage(1); }; return (
{loading ? : null} {!loading && error ? : null} {!loading && !error && !items.length ? : null} {!loading && !error ? items.map((item) => ( )) : null}
来源 金币 发放时间 事件 ID
{error}
当前无数据
{platformGrantSourceLabel(item.event_type)} {formatOptionalCoin(item.amount)} {formatDateTime(item.occurred_at_ms)} {item.event_id || "--"}
); } function ReportDialog({ children, onClose, title }) { return (

{title}

{children}
); } function ReportPagination({ onPageChange, page, pageSize, total }) { const totalPages = Math.max(1, Math.ceil(total / pageSize)); return (
共 {formatCoin(total)} 条
{page} / {totalPages}
); } function CountryCell({ row }) { return ( {row.flag ? {row.flag} : null} {row.country} ); } function DateCell({ row }) { const childCount = Array.isArray(row.children) ? row.children.length : 0; return ( {childCount ? ( ) : null} {row.date_label || "--"} {childCount ? {childCount} 国 : null} ); } function FlowRateCell({ rate, value }) { if (isMissing(value) && isMissing(rate)) { return --; } return ( {formatOptionalCoin(value)} {formatOptionalPercent(rate)} ); } function ConsumeOutputCell({ row }) { if (isMissing(row.consume_output_ratio) && isMissing(row.consume_output_delta)) { return --; } return ( {formatOptionalPercent(row.consume_output_ratio)} 差额 {formatOptionalCoin(row.consume_output_delta)} ); } function TrendValue({ value }) { return = 0 ? "report-trend-up" : "report-trend-down"}>{formatSignedPercent(value)}; } function DeltaText({ value }) { const text = String(value || "").trim(); if (!text) { return --; } return {text.startsWith("-") ? "↓" : "↑"} {text.replace(/^[-+]/, "")}; } function ReportSkeletonRows({ colSpan }) { return Array.from({ length: 6 }).map((_, index) => ( )); } function ReportEmptyRow({ colSpan }) { return ( 当前无数据 ); } function createSummaryCards(model) { const totalRecharge = model.kpis[0] || {}; const rechargeSplit = model.kpis[1]?.subMetrics || []; const newUsers = model.kpis[2] || {}; const activeUsers = model.kpis[3] || {}; const paidSplit = model.kpis[4]?.subMetrics || []; const arpu = model.kpis[5] || {}; const arppu = model.kpis[6] || {}; const payerRate = model.sideMetrics?.find((item) => item.label === "付费转化率") || {}; return [ { delta: totalRecharge.delta, details: [ { label: "用户充值", value: rechargeSplit[0]?.value || "--" }, { label: "新用户充值", value: rechargeSplit[1]?.value || "--" } ], title: "总充值", trendMetric: totalRecharge, value: totalRecharge.value || "--", valueLabel: "总充值" }, { delta: activeUsers.delta, details: [ { label: "新增用户", value: newUsers.value || "--" }, { label: "活跃用户", value: activeUsers.value || "--" } ], title: "活跃用户", trendMetric: activeUsers, value: activeUsers.value || "--", valueLabel: "活跃用户" }, { delta: paidSplit[0]?.delta, details: [ { label: "付费用户", value: paidSplit[0]?.value || "--" }, { label: "新增付费", value: paidSplit[1]?.value || "--" } ], title: "付费转化率", trendMetric: model.kpis[4], value: payerRate.value || "--", valueLabel: "付费转化率" }, { delta: arpu.delta, details: [ { label: "ARPU", value: arpu.value || "--" }, { label: "ARPPU", value: arppu.value || "--" } ], title: "ARPPU", trendMetric: arpu, value: arppu.value || "--", valueLabel: "ARPPU" } ]; } function firstSortableColumn(columns) { const column = columns.find((item) => item.sortable !== false); return column ? { direction: column.type === "text" ? "asc" : "desc", key: column.key, type: column.type || "number" } : null; } function sortRows(rows, sortState, columns) { if (!sortState?.key) { return rows; } const column = columns.find((item) => item.key === sortState.key); if (!column) { return rows; } const multiplier = sortState.direction === "asc" ? 1 : -1; return rows .map((row, index) => ({ index, row })) .sort((left, right) => { const result = compareValues(readSortValue(left.row, column), readSortValue(right.row, column), sortState.type); return result === 0 ? left.index - right.index : result * multiplier; }) .map(({ row }) => row); } function readSortValue(row, column) { return column.sortValue ? column.sortValue(row) : row[column.key]; } function compareValues(leftValue, rightValue, type) { if (type === "text") { return textCollator.compare(String(leftValue || ""), String(rightValue || "")); } const leftNumber = Number(leftValue); const rightNumber = Number(rightValue); if (!Number.isFinite(leftNumber) && !Number.isFinite(rightNumber)) { return 0; } if (!Number.isFinite(leftNumber)) { return -1; } if (!Number.isFinite(rightNumber)) { return 1; } return leftNumber - rightNumber; } function buildPlatformGrantQuery(baseQuery = {}, row = {}) { const countryId = Number(row.country_id ?? row.countryId ?? baseQuery.countryId ?? 0); const regionId = row.region_id ?? row.regionId ?? baseQuery.regionId; return { appCode: baseQuery.appCode, countryId, endMs: baseQuery.endMs, regionId, startMs: baseQuery.startMs, statDay: row.stat_day || row.statDay || "", statTz: baseQuery.statTz }; } function platformGrantCountryRows(row, rows) { const sourceRows = row.row_type === "date_total" && Array.isArray(row.children) && row.children.length ? row.children : collectPlatformGrantCountryRows(rows); return aggregatePlatformGrantCountryRows(sourceRows) .filter((item) => Number(item.platform_grant_coin || 0) > 0) .sort((left, right) => Number(right.platform_grant_coin || 0) - Number(left.platform_grant_coin || 0)); } function collectPlatformGrantCountryRows(rows) { const out = []; rows.forEach((row) => { if (row.row_type === "country") { out.push(row); } if (Array.isArray(row.children)) { out.push(...collectPlatformGrantCountryRows(row.children)); } }); return out; } function aggregatePlatformGrantCountryRows(rows) { const grouped = new Map(); rows.forEach((row) => { const key = [row.country_id ?? row.country, row.region_id ?? ""].join(":"); const current = grouped.get(key); if (!current) { grouped.set(key, { ...row, platform_grant_coin: Number(row.platform_grant_coin || 0) }); return; } const nextStatDay = current.stat_day && row.stat_day && current.stat_day === row.stat_day ? current.stat_day : ""; grouped.set(key, { ...current, platform_grant_coin: Number(current.platform_grant_coin || 0) + Number(row.platform_grant_coin || 0), stat_day: nextStatDay }); }); return Array.from(grouped.values()); } function platformGrantUserLabel(item = {}) { return item.user?.display_user_id || item.display_user_id || item.user?.username || item.username || item.nickname || item.user_id || "用户"; } function platformGrantUserMeta(item = {}) { return [item.user?.username || item.username || item.nickname, item.user_id] .filter((value, index, values) => value && values.indexOf(value) === index) .join(" · ") || "--"; } function platformGrantSourceLabel(value) { switch (String(value || "").trim()) { case "WalletLuckyGiftRewardCredited": return "幸运礼物返奖"; case "WalletTaskRewardCredited": return "任务奖励"; case "WalletWheelRewardCredited": return "转盘奖励"; case "WalletRoomTurnoverRewardCredited": return "房间流水奖励"; case "WalletInviteActivityRewardCredited": return "邀请活动奖励"; case "WalletAgencyOpeningRewardCredited": return "代理开通奖励"; case "WalletBalanceChanged": return "资源发放"; case "WalletPlatformGrantCoinCredited": return "平台发放"; default: return value || "--"; } } function formatDateTime(value) { const ms = Number(value || 0); if (!ms) { return "--"; } return new Date(ms).toLocaleString("zh-CN", { day: "2-digit", hour: "2-digit", hour12: false, minute: "2-digit", month: "2-digit", second: "2-digit", year: "numeric" }); } function formatSignedPercent(value) { if (value === undefined || value === null || Number.isNaN(Number(value))) { return "--"; } return `${Number(value) >= 0 ? "+" : ""}${formatPercent(value)}`; } function reportRowKey(row, index) { return [row.row_id, row.country, row.name, row.label, row.pool_id, row.game_id, row.platform_code, row.value, index] .filter((item) => item !== undefined && item !== null && item !== "") .join(":"); } function reportRowClass(row) { if (row.row_type === "total") { return "report-row-total"; } if (row.row_type === "date_total") { return "report-row-date-total"; } return ""; } function reportTableRowProps(row) { if (!row.onToggle) { return {}; } const action = row.expanded ? "收起" : "展开"; const label = `${action} ${row.date_label || "日期"} 国家明细`; return { "aria-expanded": row.expanded, "aria-label": label, className: "report-row-expandable", role: "button", tabIndex: 0, onClick: row.onToggle, onKeyDown: (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); row.onToggle(); } } }; } function reportRowIndex(row, index) { if (row.row_type === "total") { return "总"; } if (row.row_type === "date_total") { return "日"; } return index + 1; } function formatOptionalMoney(value) { return isMissing(value) ? -- : formatMoneyFull(value); } function formatOptionalCoin(value) { return isMissing(value) ? -- : formatCoin(value); } function formatOptionalPercent(value) { return isMissing(value) ? -- : formatPercent(value); } function formatDuration(value) { if (isMissing(value)) { return --; } const totalSeconds = Math.max(0, Math.round(Number(value) / 1000)); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); if (hours > 0) { return `${hours}h ${minutes}m`; } return `${minutes}m`; } function isMissing(value) { return value === undefined || value === null || Number.isNaN(Number(value)); }