import FileDownloadOutlined from "@mui/icons-material/FileDownloadOutlined"; import { useMemo, useState } from "react"; import { exportCoinSellerLedger, listCoinSellerLedger } from "@/features/operations/api"; import styles from "@/features/operations/operations.module.css"; import { downloadCsv } from "@/shared/api/download"; import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; import { AdminFilterResetButton, AdminListBody, AdminListToolbar } from "@/shared/ui/AdminListLayout.jsx"; import { Button } from "@/shared/ui/Button.jsx"; import { DataState } from "@/shared/ui/DataState.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx"; import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx"; import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { formatMillis } from "@/shared/utils/time.js"; const pageSize = 50; const ledgerTypeOptions = [ { label: "后台入账", value: "admin_stock_credit" }, { label: "币商转用户", value: "seller_transfer" }, { label: "工资转币商", value: "salary_transfer_received" }, ]; const ledgerTypeLabels = { admin_stock_credit: "后台入账", salary_transfer_received: "工资转币商", seller_transfer: "币商转用户", }; const bizTypeLabels = { coin_seller_coin_compensation: "金币补偿", coin_seller_stock_purchase: "USDT进货", coin_seller_transfer: "币商转用户", manual_credit: "金币增加", salary_transfer_to_coin_seller: "工资转币商", }; const baseColumns = [ { key: "seller", label: "币商信息", render: (entry) => , width: "minmax(240px, 1.2fr)", }, { key: "ledgerType", label: "流水类型", render: (entry) => ledgerTypeLabel(entry), width: "minmax(150px, 0.75fr)", }, { key: "amount", label: "金币变动", render: (entry) => , width: "minmax(130px, 0.65fr)", }, { key: "transferUsdMinor", label: "转账金额", render: (entry) => , width: "minmax(130px, 0.65fr)", }, { key: "paidAmountMicro", label: "USDT数量", render: (entry) => , width: "minmax(130px, 0.65fr)", }, { key: "receiver", label: "收款人信息", render: (entry) => , width: "minmax(240px, 1.2fr)", }, { key: "operator", label: "操作人", render: (entry) => , width: "minmax(130px, 0.65fr)", }, { key: "sellerBalanceAfter", label: "币商余额", render: (entry) => formatNumber(entry.sellerBalanceAfter), width: "minmax(120px, 0.65fr)", }, { key: "createdAtMs", label: "转账时间", render: (entry) => formatMillis(entry.createdAtMs), width: "minmax(170px, 0.85fr)", }, ]; export function CoinSellerLedgerTable({ lockedSeller = null, sellerUserId = "" }) { const fixedSellerUserId = String(sellerUserId || lockedSeller?.userId || "").trim(); const [page, setPage] = useState(1); const [exporting, setExporting] = useState(false); const [sellerKeyword, setSellerKeyword] = useState(""); const [ledgerType, setLedgerType] = useState(""); const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" }); const { showToast } = useToast(); const filters = useMemo( () => ({ end_at_ms: timeRange.endMs || "", ledger_type: ledgerType, seller_keyword: fixedSellerUserId ? "" : sellerKeyword, seller_user_id: fixedSellerUserId, start_at_ms: timeRange.startMs || "", }), [fixedSellerUserId, ledgerType, sellerKeyword, timeRange.endMs, timeRange.startMs], ); const query = usePaginatedQuery({ errorMessage: "获取币商流水失败", fetcher: listCoinSellerLedger, filters, page, pageSize, queryKey: ["coin-seller-ledger", filters, page], }); const data = query.data || { items: [], total: 0, page, pageSize }; const items = data.items || []; const total = data.total || 0; const hasActiveFilters = Boolean(sellerKeyword || ledgerType || timeRange.startMs || timeRange.endMs); const changeSellerKeyword = (value) => { setSellerKeyword(value); setPage(1); }; const changeLedgerType = (value) => { setLedgerType(value); setPage(1); }; const changeTimeRange = (nextRange) => { setTimeRange(nextRange); setPage(1); }; const resetFilters = () => { setSellerKeyword(""); setLedgerType(""); setTimeRange({ endMs: "", startMs: "" }); setPage(1); }; const downloadLedger = async () => { setExporting(true); try { await downloadCsv(() => exportCoinSellerLedger(filters), "hyapp-coin-seller-ledger.csv"); showToast("币商流水已导出", "success"); } catch (err) { showToast(err?.message || "导出币商流水失败", "error"); } finally { setExporting(false); } }; const tableColumns = baseColumns .filter((column) => !(column.key === "seller" && fixedSellerUserId)) .map((column) => { if (column.key === "seller" && !fixedSellerUserId) { return { ...column, filter: createTextColumnFilter({ placeholder: "长 ID / 短 ID / 名称", value: sellerKeyword, onChange: changeSellerKeyword, }), }; } if (column.key === "ledgerType") { return { ...column, filter: createOptionsColumnFilter({ emptyLabel: "全部", options: ledgerTypeOptions, placeholder: "流水类型", value: ledgerType, onChange: changeLedgerType, }), }; } return { ...column, }; }); return (
} actions={ } /> 0 ? { page, pageSize: data.pageSize || pageSize, total, onPageChange: setPage, } : undefined } rowKey={(entry) => entry.entryId} />
); } function OperatorCell({ entry }) { if (entry.ledgerType !== "admin_stock_credit") { return "-"; } const operator = entry.operator || {}; const operatorId = String(operator.adminId || entry.operatorUserId || "").trim(); const name = operator.username || operator.name || operatorId || "-"; return ( {name} {operatorId && operatorId !== name ? {operatorId} : null} ); } function UserCell({ user = {} }) { return ; } function AmountValue({ entry }) { const expense = entry.direction === "expense" || Number(entry.availableDelta || 0) < 0; const sign = expense ? "-" : "+"; return ( {sign} {formatNumber(entry.amount ?? Math.abs(Number(entry.availableDelta || 0)))} ); } function ledgerTypeLabel(entry) { if (entry.bizType === "manual_credit" && (entry.direction === "expense" || Number(entry.availableDelta || 0) < 0)) { return "金币扣除"; } return bizTypeLabels[entry.bizType] || ledgerTypeLabels[entry.ledgerType] || entry.ledgerType || entry.bizType || "-"; } function formatNumber(value) { return Number(value || 0).toLocaleString("zh-CN"); } function SalaryTransferAmount({ entry }) { if (entry.bizType !== "salary_transfer_to_coin_seller" && entry.ledgerType !== "salary_transfer_received") { return "-"; } const amountMinor = Number(entry.transferUsdMinor || entry.metadata?.salary_usd_minor || 0); return amountMinor > 0 ? `$${formatUSDMinor(amountMinor)}` : "-"; } function PaidUSDTAmount({ entry }) { const stockType = entry.stockType || entry.metadata?.stock_type; if (entry.bizType !== "coin_seller_stock_purchase" && stockType !== "usdt_purchase") { return "-"; } const amountMicro = Number(entry.paidAmountMicro || entry.metadata?.paid_amount_micro || 0); return amountMicro > 0 ? formatUSDTMicro(amountMicro) : "-"; } function formatUSDMinor(value) { return (Number(value || 0) / 100).toFixed(2); } function formatUSDTMicro(value) { const amount = Number(value || 0); if (!Number.isFinite(amount) || amount <= 0) { return "0"; } const whole = Math.trunc(amount / 1_000_000); const fraction = Math.trunc(Math.abs(amount % 1_000_000)); const trimmedFraction = String(fraction).padStart(6, "0").replace(/0+$/, ""); return trimmedFraction ? `${whole}.${trimmedFraction}` : String(whole); }