import CloudSyncOutlined from "@mui/icons-material/CloudSyncOutlined"; import FileDownloadOutlined from "@mui/icons-material/FileDownloadOutlined"; import RefreshOutlined from "@mui/icons-material/RefreshOutlined"; import Button from "@mui/material/Button"; import MenuItem from "@mui/material/MenuItem"; import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; import TableCell from "@mui/material/TableCell"; import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import TextField from "@mui/material/TextField"; import { useState } from "react"; import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx"; import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx"; import { EMPTY_RECHARGE_SUMMARY, formatAmount, formatMicroMoney, formatTime, formatUsdMinor, rechargeSourceTone, rechargeTypeLabel, } from "../format.js"; const SOURCE_FILTERS = [ { label: "全部来源", value: "" }, { label: "谷歌充值", value: "google_play_recharge" }, { label: "三方充值", value: "third_party" }, { label: "币商充值", value: "coin_seller" }, ]; const KPI_CARDS = [ { filter: "", key: "total", label: "充值总和", tone: "total" }, { filter: "google_play_recharge", key: "googlePlay", label: "谷歌充值", tone: "google" }, { filter: "third_party", key: "thirdParty", label: "三方充值", tone: "thirdparty" }, { filter: "coin_seller", key: "coinSeller", label: "币商充值", tone: "coinseller" }, ]; export function FinanceRechargeDetailList({ bills, error, exporting, filters, googlePaidRefreshing, loading, onExport, onFiltersChange, onRefreshGooglePaid, onReload, options, regions, summary, summaryLoading, }) { const [showCoinColumns, setShowCoinColumns] = useState(false); const items = bills.items || []; const page = Number(bills.page || filters.page || 1); const pageSize = Number(bills.pageSize || 50); const total = Number(bills.total || 0); const hasNextPage = page * pageSize < total; const regionOptions = regions || []; const visibleColumnCount = showCoinColumns ? 9 : 7; const pendingGoogleTxIds = items .filter((item) => item.rechargeType === "google_play_recharge" && !item.paidSyncedAtMs) .map((item) => item.transactionId) .filter(Boolean); return (
onFiltersChange({ rechargeType: value })} />
onFiltersChange({ appCode: event.target.value, regionId: "" })} > 全部 APP {options.apps.map((app) => ( {app.appName || app.appCode} ))} onFiltersChange({ regionId: event.target.value })} > 全部区域 {regionOptions.map((region) => ( {region.name || region.regionCode || region.regionId} ))} onFiltersChange({ rechargeType: event.target.value })} > {SOURCE_FILTERS.map((source) => ( {source.label} ))} onFiltersChange({ timeRange })} /> onFiltersChange({ keyword: event.target.value })} />
{timePresets().map((preset) => ( ))}
{error ?
{error}
: null} {loading && !items.length ?
: null} {!loading || items.length ? ( 充值时间 APP 充值来源 充值订单号 {showCoinColumns ? 充值金币 : null} {showCoinColumns ? 金币换算 : null} 账单金额 用户实付 三方税率扣款 {items.length ? ( items.map((item) => ( {formatTime(item.createdAtMs)} {appName(item, options.apps)} {showCoinColumns ? ( {coinText(item.coinAmount)} ) : null} {showCoinColumns ? {exchangeText(item)} : null} {billAmountText(item)} {userPaidText(item)} {taxDeductionText(item)} )) ) : ( 当前筛选条件下没有账单 )}
) : null}
共 {formatAmount(total)} 笔 · 第 {page} / {Math.max(1, Math.ceil(total / pageSize))} 页
); } function RechargeKpiBand({ activeFilter, loading, onSelect, showCoins, summary }) { const data = summary || EMPTY_RECHARGE_SUMMARY; const totalUsd = Number(data.total?.usdMinorAmount) || 0; return (
{KPI_CARDS.map(({ filter, key, label, tone }) => { const bucket = data[key] || { billCount: 0, coinAmount: 0, usdMinorAmount: 0 }; const active = activeFilter === filter; return ( ); })}
); } function kpiMetaText(bucket, key, totalUsd, showCoins) { const parts = [`${formatAmount(bucket.billCount)} 笔`]; if (key !== "total" && totalUsd > 0) { const share = ((Number(bucket.usdMinorAmount) || 0) / totalUsd) * 100; parts.push(`占比 ${share.toLocaleString("zh-CN", { maximumFractionDigits: 1 })}%`); } if (showCoins) { parts.push(`${formatAmount(bucket.coinAmount)} 金币`); } return parts.join(" · "); } function SourceBadge({ rechargeType }) { return ( {rechargeTypeLabel(rechargeType)} ); } function AmountText({ children, value }) { const negative = hasNumber(value) && Number(value) < 0; return {children}; } const DAY_MS = 86_400_000; const CHINA_OFFSET_MS = 8 * 3_600_000; function chinaDayStart(timestamp) { return Math.floor((timestamp + CHINA_OFFSET_MS) / DAY_MS) * DAY_MS - CHINA_OFFSET_MS; } function timePresets() { const now = Date.now(); const todayStart = chinaDayStart(now); const chinaNow = new Date(now + CHINA_OFFSET_MS); const monthStart = Date.UTC(chinaNow.getUTCFullYear(), chinaNow.getUTCMonth(), 1) - CHINA_OFFSET_MS; return [ { label: "不限", range: { endMs: "", startMs: "" } }, { label: "今天", range: { endMs: todayStart + DAY_MS, startMs: todayStart } }, { label: "昨天", range: { endMs: todayStart, startMs: todayStart - DAY_MS } }, { label: "近7天", range: { endMs: todayStart + DAY_MS, startMs: todayStart - 6 * DAY_MS } }, { label: "本月", range: { endMs: todayStart + DAY_MS, startMs: monthStart } }, ]; } function isPresetActive(timeRange, preset) { const current = timeRange || { endMs: "", startMs: "" }; return ( String(current.startMs || "") === String(preset.range.startMs || "") && String(current.endMs || "") === String(preset.range.endMs || "") ); } function Stacked({ primary, secondary }) { return ( {primary || "-"} {secondary || ""} ); } function appName(item, apps) { return item.appName || apps.find((app) => app.appCode === item.appCode)?.appName || item.appCode || "-"; } function secondaryOrderNo(item) { return [item.commandId, item.externalRef].filter(Boolean).join(" / "); } function coinText(value) { const text = formatAmount(value); return text === "-" ? "-" : `${text} 金币`; } function exchangeText(item) { if (hasNumber(item.exchangeCoinAmount) || hasNumber(item.exchangeUsdMinorAmount)) { return `${coinText(item.exchangeCoinAmount)} = ${formatUsdMinor(item.exchangeUsdMinorAmount, "USD")}`; } return formatUsdMinor(item.usdMinorAmount, "USD"); } function billAmountText(item) { if (item.billAmountText) { return item.billAmountText; } const amount = hasNumber(item.billAmountMinor) ? item.billAmountMinor : hasPositiveNumber(item.providerAmountMinor) ? item.providerAmountMinor : item.usdMinorAmount; return formatUsdMinor(amount, item.currencyCode || "USD"); } function userPaidText(item) { if (item.userPaidText) { return item.userPaidText; } if (hasNonZeroNumber(item.userPaidAmountMicro) && item.userPaidCurrencyCode) { return formatMicroMoney(item.userPaidAmountMicro, item.userPaidCurrencyCode); } if (isGoogleBill(item) && !item.paidSyncedAtMs) { return "未同步(点“查询谷歌实付”)"; } if (hasPositiveNumber(item.userPaidAmountMinor)) { return formatUsdMinor(item.userPaidAmountMinor, item.userPaidCurrencyCode || item.currencyCode || "USD"); } return "-"; } function taxDeductionText(item) { const currency = item.userPaidCurrencyCode || item.currencyCode || "USD"; const fee = hasPositiveNumber(item.providerFeeMicro) ? item.providerFeeMicro : 0; const tax = hasPositiveNumber(item.providerTaxMicro) ? item.providerTaxMicro : 0; const deduction = fee + tax; if (deduction > 0) { const parts = [formatMicroMoney(deduction, currency)]; if (hasPositiveNumber(item.userPaidAmountMicro)) { const percent = (deduction / item.userPaidAmountMicro) * 100; parts.push(`${percent.toLocaleString("zh-CN", { maximumFractionDigits: 2 })}%`); } return ( {parts.join(" / ")} {deductionBreakdown(fee, tax, currency)} ); } if (item.taxDeductionText) { return item.taxDeductionText; } if (isGoogleBill(item) && !item.paidSyncedAtMs) { return "未同步"; } return "-"; } function deductionBreakdown(fee, tax, currency) { const parts = []; if (fee > 0) { parts.push(`手续费 ${formatMicroMoney(fee, currency)}`); } if (tax > 0) { parts.push(`税费 ${formatMicroMoney(tax, currency)}`); } return parts.join(" + "); } function isGoogleBill(item) { return item.rechargeType === "google_play_recharge" || item.providerCode === "google_play"; } function hasNumber(value) { if (value === null || value === undefined || value === "") { return false; } return Number.isFinite(Number(value)); } function hasPositiveNumber(value) { return hasNumber(value) && Number(value) > 0; } function hasNonZeroNumber(value) { return hasNumber(value) && Number(value) !== 0; }