2026-07-09 16:16:31 +08:00

326 lines
12 KiB
JavaScript

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, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
import { useToast } from "@/shared/ui/ToastProvider.jsx";
import { emptyUserIdentityFilter, isUserIdentityFilterEmpty, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
import { formatMillis } from "@/shared/utils/time.js";
const pageSize = 50;
const ledgerTypeOptions = [
{ label: "后台入账", value: "admin_stock_credit" },
{ label: "币商转用户", value: "seller_transfer" },
{ label: "向子币商转账", value: "sub_seller_transfer" },
{ label: "工资转币商", value: "salary_transfer_received" },
];
const ledgerTypeLabels = {
admin_stock_credit: "后台入账",
salary_transfer_received: "工资转币商",
seller_transfer: "币商转用户",
sub_seller_transfer: "向子币商转账",
};
const bizTypeLabels = {
coin_seller_coin_compensation: "金币补偿",
coin_seller_stock_deduction: "USDT扣除",
coin_seller_stock_purchase: "USDT进货",
coin_seller_sub_transfer: "向子币商转账",
coin_seller_transfer: "币商转用户",
manual_credit: "金币增加",
salary_transfer_to_coin_seller: "工资转币商",
};
const baseColumns = [
{
key: "seller",
label: "币商信息",
render: (entry) => <UserCell user={entry.seller} />,
width: "minmax(240px, 1.2fr)",
},
{
key: "ledgerType",
label: "流水类型",
render: (entry) => ledgerTypeLabel(entry),
width: "minmax(150px, 0.75fr)",
},
{
key: "amount",
label: "金币变动",
render: (entry) => <AmountValue entry={entry} />,
width: "minmax(130px, 0.65fr)",
},
{
key: "transferUsdMinor",
label: "转账金额",
render: (entry) => <SalaryTransferAmount entry={entry} />,
width: "minmax(130px, 0.65fr)",
},
{
key: "paidAmountMicro",
label: "USDT数量",
render: (entry) => <PaidUSDTAmount entry={entry} />,
width: "minmax(130px, 0.65fr)",
},
{
key: "receiver",
label: "收款人信息",
render: (entry) => <UserCell user={entry.receiver} />,
width: "minmax(240px, 1.2fr)",
},
{
key: "operator",
label: "操作人",
render: (entry) => <OperatorCell entry={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 [sellerFilter, setSellerFilter] = useState(emptyUserIdentityFilter);
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_user_id: fixedSellerUserId,
start_at_ms: timeRange.startMs || "",
...userIdentityFilterParams(sellerFilter, {
displayUserId: "seller_display_user_id",
userId: "seller_filter_user_id",
username: "seller_username",
}),
}),
[fixedSellerUserId, ledgerType, sellerFilter, 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(!isUserIdentityFilterEmpty(sellerFilter) || ledgerType || timeRange.startMs || timeRange.endMs);
const changeSellerFilter = (value) => {
setSellerFilter(value);
setPage(1);
};
const changeLedgerType = (value) => {
setLedgerType(value);
setPage(1);
};
const changeTimeRange = (nextRange) => {
setTimeRange(nextRange);
setPage(1);
};
const resetFilters = () => {
setSellerFilter(emptyUserIdentityFilter);
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: createUserIdentityColumnFilter({
value: sellerFilter,
onChange: changeSellerFilter,
}),
};
}
if (column.key === "ledgerType") {
return {
...column,
filter: createOptionsColumnFilter({
emptyLabel: "全部",
options: ledgerTypeOptions,
placeholder: "流水类型",
value: ledgerType,
onChange: changeLedgerType,
}),
};
}
return {
...column,
};
});
return (
<div className={styles.ledgerTableShell}>
<AdminListToolbar
filters={
<>
<TimeRangeFilter value={timeRange} onChange={changeTimeRange} />
<AdminFilterResetButton disabled={!hasActiveFilters} onClick={resetFilters} />
</>
}
actions={
<Button
disabled={exporting}
startIcon={<FileDownloadOutlined fontSize="small" />}
onClick={downloadLedger}
>
{exporting ? "导出中" : "导出"}
</Button>
}
/>
<DataState error={query.error} loading={query.loading} onRetry={query.reload}>
<AdminListBody>
<DataTable
columns={tableColumns}
emptyLabel="当前无币商流水"
items={items}
minWidth={fixedSellerUserId ? "1130px" : "1380px"}
pagination={
total > 0
? {
page,
pageSize: data.pageSize || pageSize,
total,
onPageChange: setPage,
}
: undefined
}
rowKey={(entry) => entry.entryId}
/>
</AdminListBody>
</DataState>
</div>
);
}
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 (
<span className={styles.identityText}>
<span className={styles.primaryText}>{name}</span>
{operatorId && operatorId !== name ? <span className={styles.meta}>{operatorId}</span> : null}
</span>
);
}
function UserCell({ user = {} }) {
return <AdminUserIdentity openInAppUserDetail user={user} />;
}
function AmountValue({ entry }) {
const expense = entry.direction === "expense" || Number(entry.availableDelta || 0) < 0;
const sign = expense ? "-" : "+";
return (
<span className={`${styles.amount} ${expense ? styles.amountExpense : styles.amountIncome}`}>
{sign}
{formatNumber(entry.amount ?? Math.abs(Number(entry.availableDelta || 0)))}
</span>
);
}
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;
const isUSDTStock =
entry.bizType === "coin_seller_stock_purchase" ||
entry.bizType === "coin_seller_stock_deduction" ||
stockType === "usdt_purchase" ||
stockType === "usdt_deduction";
if (!isUSDTStock) {
return "-";
}
const amountMicro = Number(entry.paidAmountMicro || entry.metadata?.paid_amount_micro || 0);
return amountMicro !== 0 ? formatSignedUSDTMicro(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);
}
function formatSignedUSDTMicro(value) {
const amount = Number(value || 0);
if (!Number.isFinite(amount) || amount === 0) {
return "0";
}
const sign = amount < 0 ? "-" : "";
return `${sign}${formatUSDTMicro(Math.abs(amount))}`;
}