265 lines
7.9 KiB
JavaScript
265 lines
7.9 KiB
JavaScript
import { useMemo, useState } from "react";
|
|
import { listRechargeBills } from "@/features/payment/api";
|
|
import {
|
|
AdminListBody,
|
|
AdminListPage,
|
|
} from "@/shared/ui/AdminListLayout.jsx";
|
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
|
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
|
import { formatMillis } from "@/shared/utils/time.js";
|
|
|
|
const pageSize = 20;
|
|
const statusOptions = [
|
|
["", "全部状态"],
|
|
["succeeded", "成功"],
|
|
];
|
|
const rechargeTypeOptions = [
|
|
["", "全部途径"],
|
|
["coin_seller_transfer", "币商充值"],
|
|
];
|
|
|
|
const columns = [
|
|
{
|
|
key: "bill",
|
|
label: "账单",
|
|
width: "minmax(240px, 1.4fr)",
|
|
render: (bill) => <Stack primary={bill.transactionId} secondary={bill.commandId || bill.externalRef || "-"} />,
|
|
},
|
|
{
|
|
key: "userId",
|
|
label: "用户 ID",
|
|
width: "minmax(120px, 0.65fr)",
|
|
render: (bill) => bill.userId || "-",
|
|
},
|
|
{
|
|
key: "sellerUserId",
|
|
label: "币商 ID",
|
|
width: "minmax(120px, 0.65fr)",
|
|
render: (bill) => bill.sellerUserId || "-",
|
|
},
|
|
{
|
|
key: "amount",
|
|
label: "金币 / 金额",
|
|
width: "minmax(150px, 0.75fr)",
|
|
render: (bill) => (
|
|
<Stack
|
|
primary={formatNumber(bill.coinAmount)}
|
|
secondary={formatMoney(bill.usdMinorAmount, bill.currencyCode)}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
key: "policy",
|
|
label: "兑换口径",
|
|
width: "minmax(170px, 0.9fr)",
|
|
render: (bill) => (
|
|
<Stack
|
|
primary={bill.policyVersion || "-"}
|
|
secondary={`${formatNumber(bill.exchangeCoinAmount)} / ${formatMoney(bill.exchangeUsdMinorAmount, bill.currencyCode)}`}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
key: "region",
|
|
label: "区域",
|
|
width: "minmax(110px, 0.55fr)",
|
|
render: (bill) => (
|
|
<Stack
|
|
primary={bill.targetRegionId ? `区域 ${bill.targetRegionId}` : "-"}
|
|
secondary={bill.sellerRegionId ? `来源 ${bill.sellerRegionId}` : "-"}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
key: "rechargeType",
|
|
label: "充值途径",
|
|
width: "minmax(130px, 0.7fr)",
|
|
render: (bill) => rechargeTypeLabel(bill.rechargeType),
|
|
},
|
|
{
|
|
key: "status",
|
|
label: "状态",
|
|
width: "minmax(100px, 0.55fr)",
|
|
render: (bill) => <StatusBadge status={bill.status} />,
|
|
},
|
|
{
|
|
key: "time",
|
|
label: "创建时间",
|
|
width: "minmax(160px, 0.8fr)",
|
|
render: (bill) => formatMillis(bill.createdAtMs),
|
|
},
|
|
];
|
|
|
|
export function PaymentBillListPage() {
|
|
const [page, setPage] = useState(1);
|
|
const [keyword, setKeyword] = useState("");
|
|
const [status, setStatus] = useState("");
|
|
const [rechargeType, setRechargeType] = useState("");
|
|
const [userId, setUserId] = useState("");
|
|
const [sellerUserId, setSellerUserId] = useState("");
|
|
|
|
const filters = useMemo(
|
|
() => ({
|
|
keyword,
|
|
recharge_type: rechargeType,
|
|
seller_user_id: sellerUserId,
|
|
status,
|
|
user_id: userId,
|
|
}),
|
|
[keyword, rechargeType, sellerUserId, status, userId],
|
|
);
|
|
const query = usePaginatedQuery({
|
|
errorMessage: "获取账单列表失败",
|
|
fetcher: listRechargeBills,
|
|
filters,
|
|
page,
|
|
pageSize,
|
|
queryKey: ["payment-bills", filters, page],
|
|
});
|
|
const data = query.data || { items: [], total: 0, page, pageSize };
|
|
const items = data.items || [];
|
|
const total = data.total || 0;
|
|
|
|
const resetPage = (setter) => (value) => {
|
|
setter(value);
|
|
setPage(1);
|
|
};
|
|
|
|
const tableColumns = columns.map((column) => {
|
|
if (column.key === "bill") {
|
|
return {
|
|
...column,
|
|
filter: createTextColumnFilter({
|
|
placeholder: "搜索账单号、命令号、外部单号",
|
|
value: keyword,
|
|
onChange: resetPage(setKeyword),
|
|
}),
|
|
};
|
|
}
|
|
if (column.key === "userId") {
|
|
return {
|
|
...column,
|
|
filter: createTextColumnFilter({
|
|
placeholder: "搜索用户 ID",
|
|
value: userId,
|
|
onChange: (value) => resetPage(setUserId)(digitsOnly(value)),
|
|
}),
|
|
};
|
|
}
|
|
if (column.key === "sellerUserId") {
|
|
return {
|
|
...column,
|
|
filter: createTextColumnFilter({
|
|
placeholder: "搜索币商 ID",
|
|
value: sellerUserId,
|
|
onChange: (value) => resetPage(setSellerUserId)(digitsOnly(value)),
|
|
}),
|
|
};
|
|
}
|
|
if (column.key === "rechargeType") {
|
|
return {
|
|
...column,
|
|
filter: createOptionsColumnFilter({
|
|
options: rechargeTypeOptions,
|
|
placeholder: "搜索充值途径",
|
|
value: rechargeType,
|
|
onChange: resetPage(setRechargeType),
|
|
}),
|
|
};
|
|
}
|
|
if (column.key === "status") {
|
|
return {
|
|
...column,
|
|
filter: createOptionsColumnFilter({
|
|
options: statusOptions,
|
|
placeholder: "搜索状态",
|
|
value: status,
|
|
onChange: resetPage(setStatus),
|
|
}),
|
|
};
|
|
}
|
|
return column;
|
|
});
|
|
|
|
return (
|
|
<AdminListPage>
|
|
<DataState error={query.error} loading={query.loading} onRetry={query.reload}>
|
|
<AdminListBody>
|
|
<DataTable
|
|
columns={tableColumns}
|
|
items={items}
|
|
minWidth="1260px"
|
|
pagination={
|
|
total > 0
|
|
? {
|
|
page,
|
|
pageSize: data.pageSize || pageSize,
|
|
total,
|
|
onPageChange: setPage,
|
|
}
|
|
: undefined
|
|
}
|
|
rowKey={(bill) => bill.transactionId}
|
|
/>
|
|
</AdminListBody>
|
|
</DataState>
|
|
</AdminListPage>
|
|
);
|
|
}
|
|
|
|
function Stack({ primary, secondary }) {
|
|
return (
|
|
<div className="cell-stack">
|
|
<span>{primary || "-"}</span>
|
|
<span className="muted">{secondary || "-"}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatusBadge({ status }) {
|
|
const tone = status === "succeeded" ? "succeeded" : status === "failed" ? "danger" : "stopped";
|
|
return (
|
|
<span className={`status-badge status-badge--${tone}`}>
|
|
<span className="status-point" />
|
|
{statusLabel(status)}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function rechargeTypeLabel(value) {
|
|
return rechargeTypeOptions.find(([optionValue]) => optionValue === value)?.[1] || value || "-";
|
|
}
|
|
|
|
function statusLabel(value) {
|
|
if (value === "succeeded") {
|
|
return "成功";
|
|
}
|
|
if (value === "failed") {
|
|
return "失败";
|
|
}
|
|
return value || "-";
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
if (value === 0 || value) {
|
|
return Number(value).toLocaleString("zh-CN");
|
|
}
|
|
return "-";
|
|
}
|
|
|
|
function formatMoney(value, currency = "USD") {
|
|
if (!(value === 0 || value)) {
|
|
return "-";
|
|
}
|
|
return `${currency || "USD"} ${(Number(value) / 100).toLocaleString("zh-CN", {
|
|
maximumFractionDigits: 2,
|
|
minimumFractionDigits: 2,
|
|
})}`;
|
|
}
|
|
|
|
function digitsOnly(value) {
|
|
return value.replace(/\D/g, "");
|
|
}
|