主播提现列表
This commit is contained in:
parent
8fe2efb344
commit
18ad13563b
@ -1650,6 +1650,71 @@
|
||||
"x-permissions": ["coin-seller:exchange-rate"]
|
||||
}
|
||||
},
|
||||
"/admin/host-withdrawals": {
|
||||
"get": {
|
||||
"operationId": "listHostWithdrawals",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/Page"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/PageSize"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "app_code",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "record_type",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["salary_transfer", "withdrawal"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "identity",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["host", "agency", "bd", "bd_leader_admin"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/Status"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/Keyword"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "start_at_ms",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "end_at_ms",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/HostWithdrawalPageResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "host-withdrawal:view",
|
||||
"x-permissions": ["host-withdrawal:view"]
|
||||
}
|
||||
},
|
||||
"/admin/countries": {
|
||||
"get": {
|
||||
"operationId": "listCountries",
|
||||
@ -5819,6 +5884,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"HostWithdrawalPageResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseHostWithdrawalPage"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"HostProfilePageResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
@ -6159,6 +6234,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiPageHostWithdrawal": {
|
||||
"type": "object",
|
||||
"required": ["items", "page", "pageSize", "total"],
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/HostWithdrawal"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiPageRechargeBill": {
|
||||
"type": "object",
|
||||
"required": ["items", "page", "pageSize", "total"],
|
||||
@ -6349,6 +6445,21 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseHostWithdrawalPage": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Envelope"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/ApiPageHostWithdrawal"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseRechargeBillPage": {
|
||||
"allOf": [
|
||||
{
|
||||
@ -6852,6 +6963,114 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"HostWithdrawalUser": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"avatar": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HostWithdrawal": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"recordType",
|
||||
"identity",
|
||||
"salaryAssetType",
|
||||
"usdMinorAmount",
|
||||
"coinAmount",
|
||||
"status",
|
||||
"createdAtMs",
|
||||
"updatedAtMs"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"recordType": {
|
||||
"type": "string",
|
||||
"enum": ["salary_transfer", "withdrawal"]
|
||||
},
|
||||
"identity": {
|
||||
"type": "string"
|
||||
},
|
||||
"salaryAssetType": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceUser": {
|
||||
"$ref": "#/components/schemas/HostWithdrawalUser"
|
||||
},
|
||||
"sellerUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sellerUser": {
|
||||
"$ref": "#/components/schemas/HostWithdrawalUser"
|
||||
},
|
||||
"usdMinorAmount": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"coinAmount": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"transactionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"commandId": {
|
||||
"type": "string"
|
||||
},
|
||||
"applicationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"freezeTransactionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"auditTransactionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"withdrawMethod": {
|
||||
"type": "string"
|
||||
},
|
||||
"withdrawAddress": {
|
||||
"type": "string"
|
||||
},
|
||||
"auditRemark": {
|
||||
"type": "string"
|
||||
},
|
||||
"auditImageUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"createdAtMs": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"updatedAtMs": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HumanRoomRobotCountryRule": {
|
||||
"type": "object",
|
||||
"required": ["countryCode", "maxRoomCount"],
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { API_OPERATIONS, apiEndpointPath } from "../../src/shared/api/generated/endpoints.ts";
|
||||
import { API_BASE_URL } from "../../src/shared/config/env.js";
|
||||
import { countryFlag, stripCountryFlagPrefix } from "./data/countryMeta.js";
|
||||
|
||||
const API_BASE_URL = import.meta.env?.VITE_API_BASE_URL || "/api";
|
||||
const TOKEN_KEY = "hyapp-admin.access-token";
|
||||
const APP_CODE_KEY = "hyapp-admin.app-code";
|
||||
const APP_CODE_HEADER = "X-App-Code";
|
||||
|
||||
@ -134,6 +134,7 @@ export const fallbackNavigation = [
|
||||
routeNavItem("host-agency-policy", { icon: WalletOutlined }),
|
||||
routeNavItem("host-salary-settlement", { icon: ReceiptLongOutlined }),
|
||||
routeNavItem("host-org-coin-sellers", { icon: WalletOutlined }),
|
||||
routeNavItem("host-withdrawals", { icon: ReceiptLongOutlined }),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -41,6 +41,7 @@ export const PERMISSIONS = {
|
||||
coinSellerUpdate: "coin-seller:update",
|
||||
coinSellerStockCredit: "coin-seller:stock-credit",
|
||||
coinSellerExchangeRate: "coin-seller:exchange-rate",
|
||||
hostWithdrawalView: "host-withdrawal:view",
|
||||
financeView: "finance:view",
|
||||
financeApplicationCreate: "finance-application:create",
|
||||
financeApplicationAudit: "finance-application:audit",
|
||||
@ -272,6 +273,7 @@ export const MENU_CODES = {
|
||||
hostAgencyPolicy: "host-agency-policy",
|
||||
hostSalarySettlement: "host-salary-settlement",
|
||||
hostOrgCoinSellers: "host-org-coin-sellers",
|
||||
hostWithdrawals: "host-withdrawals",
|
||||
payment: "payment",
|
||||
paymentBillList: "payment-bill-list",
|
||||
paymentRechargeProducts: "payment-recharge-products",
|
||||
|
||||
@ -30,6 +30,7 @@ import type {
|
||||
CreateManagerPayload,
|
||||
EntityId,
|
||||
HostCommandPayload,
|
||||
HostWithdrawalDto,
|
||||
HostProfileDto,
|
||||
ManagerDto,
|
||||
PageQuery,
|
||||
@ -212,6 +213,14 @@ export function listCoinSellers(query: PageQuery = {}): Promise<ApiPage<CoinSell
|
||||
});
|
||||
}
|
||||
|
||||
export function listHostWithdrawals(query: PageQuery = {}): Promise<ApiPage<HostWithdrawalDto>> {
|
||||
const endpoint = API_ENDPOINTS.listHostWithdrawals;
|
||||
return apiRequest<ApiPage<HostWithdrawalDto>>(apiEndpointPath(API_OPERATIONS.listHostWithdrawals), {
|
||||
method: endpoint.method,
|
||||
query,
|
||||
});
|
||||
}
|
||||
|
||||
export function listSalaryWalletHistory(query: PageQuery = {}): Promise<ApiPage<SalaryWalletHistoryDto>> {
|
||||
return apiRequest<ApiPage<SalaryWalletHistoryDto>>("/v1/admin/host-org/salary-wallets/history", {
|
||||
method: "GET",
|
||||
|
||||
90
src/features/host-org/hooks/useHostWithdrawalsPage.js
Normal file
90
src/features/host-org/hooks/useHostWithdrawalsPage.js
Normal file
@ -0,0 +1,90 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { listHostWithdrawals } from "@/features/host-org/api";
|
||||
import { useHostWithdrawalAbilities } from "@/features/host-org/permissions.js";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
|
||||
const pageSize = 50;
|
||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||
const emptyTimeRange = { endMs: "", startMs: "" };
|
||||
|
||||
export function useHostWithdrawalsPage() {
|
||||
const abilities = useHostWithdrawalAbilities();
|
||||
const [page, setPage] = useState(1);
|
||||
const [recordType, setRecordType] = useState("");
|
||||
const [identity, setIdentity] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [timeRange, setTimeRange] = useState(emptyTimeRange);
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
end_at_ms: timeRange.endMs || "",
|
||||
identity,
|
||||
keyword,
|
||||
record_type: recordType,
|
||||
start_at_ms: timeRange.startMs || "",
|
||||
status,
|
||||
}),
|
||||
[identity, keyword, recordType, status, timeRange.endMs, timeRange.startMs],
|
||||
);
|
||||
const query = usePaginatedQuery({
|
||||
errorMessage: "加载主播提现列表失败",
|
||||
fetcher: listHostWithdrawals,
|
||||
filters,
|
||||
page,
|
||||
pageSize,
|
||||
queryKey: ["host-org", "host-withdrawals", filters, page],
|
||||
});
|
||||
const data = query.data || emptyData;
|
||||
const hasActiveFilters = Boolean(recordType || identity || status || keyword || timeRange.startMs || timeRange.endMs);
|
||||
|
||||
const changeRecordType = (value) => {
|
||||
setRecordType(value);
|
||||
setPage(1);
|
||||
};
|
||||
const changeIdentity = (value) => {
|
||||
setIdentity(value);
|
||||
setPage(1);
|
||||
};
|
||||
const changeStatus = (value) => {
|
||||
setStatus(value);
|
||||
setPage(1);
|
||||
};
|
||||
const changeKeyword = (value) => {
|
||||
setKeyword(value);
|
||||
setPage(1);
|
||||
};
|
||||
const changeTimeRange = (nextRange) => {
|
||||
setTimeRange(nextRange);
|
||||
setPage(1);
|
||||
};
|
||||
const resetFilters = () => {
|
||||
setRecordType("");
|
||||
setIdentity("");
|
||||
setStatus("");
|
||||
setKeyword("");
|
||||
setTimeRange(emptyTimeRange);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
return {
|
||||
abilities,
|
||||
changeIdentity,
|
||||
changeKeyword,
|
||||
changeRecordType,
|
||||
changeStatus,
|
||||
changeTimeRange,
|
||||
data,
|
||||
error: query.error,
|
||||
hasActiveFilters,
|
||||
identity,
|
||||
keyword,
|
||||
loading: query.loading,
|
||||
page,
|
||||
recordType,
|
||||
reload: query.reload,
|
||||
resetFilters,
|
||||
setPage,
|
||||
status,
|
||||
timeRange,
|
||||
};
|
||||
}
|
||||
276
src/features/host-org/pages/HostWithdrawalsPage.jsx
Normal file
276
src/features/host-org/pages/HostWithdrawalsPage.jsx
Normal file
@ -0,0 +1,276 @@
|
||||
import {
|
||||
AdminFilterResetButton,
|
||||
AdminListBody,
|
||||
AdminListPage,
|
||||
AdminListToolbar,
|
||||
} from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import { useHostWithdrawalsPage } from "@/features/host-org/hooks/useHostWithdrawalsPage.js";
|
||||
import styles from "@/features/operations/operations.module.css";
|
||||
|
||||
const recordTypeOptions = [
|
||||
["", "全部"],
|
||||
["salary_transfer", "工资转币商"],
|
||||
["withdrawal", "提现申请"],
|
||||
];
|
||||
|
||||
const identityOptions = [
|
||||
["", "全部"],
|
||||
["host", "Host"],
|
||||
["agency", "Agency"],
|
||||
["bd", "BD"],
|
||||
["bd_leader_admin", "BD Leader/Admin"],
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
["", "全部"],
|
||||
["completed", "已完成"],
|
||||
["pending", "待审核"],
|
||||
["approved", "已通过"],
|
||||
["rejected", "已拒绝"],
|
||||
];
|
||||
|
||||
const recordTypeLabels = Object.fromEntries(recordTypeOptions.filter(([value]) => value));
|
||||
const identityLabels = Object.fromEntries(identityOptions.filter(([value]) => value));
|
||||
const statusLabels = Object.fromEntries(statusOptions.filter(([value]) => value));
|
||||
|
||||
const baseColumns = [
|
||||
{
|
||||
key: "createdAtMs",
|
||||
label: "时间",
|
||||
render: (item) => formatMillis(item.createdAtMs),
|
||||
width: "minmax(170px, 0.8fr)",
|
||||
},
|
||||
{
|
||||
key: "recordType",
|
||||
label: "类型",
|
||||
render: (item) => recordTypeLabel(item.recordType),
|
||||
width: "minmax(130px, 0.6fr)",
|
||||
},
|
||||
{
|
||||
key: "identity",
|
||||
label: "身份",
|
||||
render: (item) => identityLabel(item.identity, item.salaryAssetType),
|
||||
width: "minmax(150px, 0.65fr)",
|
||||
},
|
||||
{
|
||||
key: "sourceUser",
|
||||
label: "发起用户",
|
||||
render: (item) => <AdminUserIdentity openInAppUserDetail user={item.sourceUser} userId={item.sourceUserId} />,
|
||||
width: "minmax(230px, 1fr)",
|
||||
},
|
||||
{
|
||||
key: "receiver",
|
||||
label: "币商/收款",
|
||||
render: (item) => <ReceiverCell item={item} />,
|
||||
width: "minmax(240px, 1.05fr)",
|
||||
},
|
||||
{
|
||||
key: "usdMinorAmount",
|
||||
label: "USD金额",
|
||||
render: (item) => <AmountText value={`$${formatUSDMinor(item.usdMinorAmount)}`} />,
|
||||
width: "minmax(120px, 0.55fr)",
|
||||
},
|
||||
{
|
||||
key: "coinAmount",
|
||||
label: "币商金币",
|
||||
render: (item) => formatCoinAmount(item),
|
||||
width: "minmax(130px, 0.6fr)",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
render: (item) => <StatusBadge status={item.status} />,
|
||||
width: "minmax(110px, 0.55fr)",
|
||||
},
|
||||
{
|
||||
key: "recordId",
|
||||
label: "交易/申请ID",
|
||||
render: (item) => <RecordIdCell item={item} />,
|
||||
width: "minmax(260px, 1.1fr)",
|
||||
},
|
||||
];
|
||||
|
||||
export function HostWithdrawalsPage() {
|
||||
const page = useHostWithdrawalsPage();
|
||||
const data = page.data || { items: [], page: 1, pageSize: 50, total: 0 };
|
||||
const items = data.items || [];
|
||||
const total = Number(data.total || 0);
|
||||
const tableColumns = baseColumns.map((column) => {
|
||||
if (column.key === "recordType") {
|
||||
return {
|
||||
...column,
|
||||
filter: createOptionsColumnFilter({
|
||||
emptyLabel: "全部",
|
||||
options: recordTypeOptions,
|
||||
placeholder: "类型",
|
||||
value: page.recordType,
|
||||
onChange: page.changeRecordType,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (column.key === "identity") {
|
||||
return {
|
||||
...column,
|
||||
filter: createOptionsColumnFilter({
|
||||
emptyLabel: "全部",
|
||||
options: identityOptions,
|
||||
placeholder: "身份",
|
||||
value: page.identity,
|
||||
onChange: page.changeIdentity,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (column.key === "sourceUser") {
|
||||
return {
|
||||
...column,
|
||||
filter: createTextColumnFilter({
|
||||
activeLabel: page.keyword ? `用户 ${page.keyword}` : "",
|
||||
placeholder: "用户/交易/地址",
|
||||
value: page.keyword,
|
||||
onChange: page.changeKeyword,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (column.key === "status") {
|
||||
return {
|
||||
...column,
|
||||
filter: createOptionsColumnFilter({
|
||||
emptyLabel: "全部",
|
||||
options: statusOptions,
|
||||
placeholder: "状态",
|
||||
value: page.status,
|
||||
onChange: page.changeStatus,
|
||||
}),
|
||||
};
|
||||
}
|
||||
return column;
|
||||
});
|
||||
|
||||
return (
|
||||
<AdminListPage>
|
||||
<AdminListToolbar
|
||||
filters={
|
||||
<>
|
||||
<TimeRangeFilter value={page.timeRange} onChange={page.changeTimeRange} />
|
||||
<AdminFilterResetButton disabled={!page.hasActiveFilters} onClick={page.resetFilters} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||
<AdminListBody>
|
||||
<DataTable
|
||||
columns={tableColumns}
|
||||
emptyLabel="当前无主播提现记录"
|
||||
items={items}
|
||||
minWidth="1510px"
|
||||
pagination={
|
||||
total > 0
|
||||
? {
|
||||
page: page.page,
|
||||
pageSize: data.pageSize || 50,
|
||||
total,
|
||||
onPageChange: page.setPage,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
rowKey={(item) => item.id}
|
||||
/>
|
||||
</AdminListBody>
|
||||
</DataState>
|
||||
</AdminListPage>
|
||||
);
|
||||
}
|
||||
|
||||
function ReceiverCell({ item }) {
|
||||
if (item.recordType === "salary_transfer") {
|
||||
return <AdminUserIdentity openInAppUserDetail user={item.sellerUser} userId={item.sellerUserId} />;
|
||||
}
|
||||
const method = String(item.withdrawMethod || "").trim();
|
||||
const address = String(item.withdrawAddress || "").trim();
|
||||
if (!method && !address) {
|
||||
return "-";
|
||||
}
|
||||
return (
|
||||
<span className={styles.identityText}>
|
||||
<span className={styles.primaryText}>{method || "-"}</span>
|
||||
{address ? <span className={styles.meta}>{address}</span> : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function RecordIdCell({ item }) {
|
||||
const primary = item.transactionId || item.applicationId || item.id || "-";
|
||||
const secondary = item.commandId || item.freezeTransactionId || item.auditTransactionId || "";
|
||||
return (
|
||||
<span className={styles.identityText}>
|
||||
<span className={styles.primaryText}>{primary}</span>
|
||||
{secondary ? <span className={styles.meta}>{secondary}</span> : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ status }) {
|
||||
const normalized = String(status || "").trim();
|
||||
return (
|
||||
<span className={`status-badge status-badge--${statusTone(normalized)}`}>
|
||||
<span className="status-point" />
|
||||
{statusLabel(normalized)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function AmountText({ value }) {
|
||||
return <span className={styles.amount}>{value}</span>;
|
||||
}
|
||||
|
||||
function recordTypeLabel(value) {
|
||||
return recordTypeLabels[value] || value || "-";
|
||||
}
|
||||
|
||||
function identityLabel(identity, salaryAssetType) {
|
||||
return identityLabels[identity] || salaryAssetLabel(salaryAssetType) || identity || "-";
|
||||
}
|
||||
|
||||
function salaryAssetLabel(assetType) {
|
||||
const labels = {
|
||||
AGENCY_SALARY_USD: "Agency",
|
||||
ADMIN_SALARY_USD: "BD Leader/Admin",
|
||||
BD_SALARY_USD: "BD",
|
||||
HOST_SALARY_USD: "Host",
|
||||
};
|
||||
return labels[assetType] || "";
|
||||
}
|
||||
|
||||
function statusLabel(status) {
|
||||
return statusLabels[status] || status || "-";
|
||||
}
|
||||
|
||||
function statusTone(status) {
|
||||
if (status === "completed" || status === "approved") {
|
||||
return "succeeded";
|
||||
}
|
||||
if (status === "pending") {
|
||||
return "warning";
|
||||
}
|
||||
if (status === "rejected") {
|
||||
return "danger";
|
||||
}
|
||||
return "stopped";
|
||||
}
|
||||
|
||||
function formatCoinAmount(item) {
|
||||
if (item.recordType !== "salary_transfer") {
|
||||
return "-";
|
||||
}
|
||||
return Number(item.coinAmount || 0).toLocaleString("zh-CN");
|
||||
}
|
||||
|
||||
function formatUSDMinor(value) {
|
||||
return (Number(value || 0) / 100).toFixed(2);
|
||||
}
|
||||
175
src/features/host-org/pages/HostWithdrawalsPage.test.jsx
Normal file
175
src/features/host-org/pages/HostWithdrawalsPage.test.jsx
Normal file
@ -0,0 +1,175 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { HostWithdrawalsPage } from "./HostWithdrawalsPage.jsx";
|
||||
import { useHostWithdrawalsPage } from "@/features/host-org/hooks/useHostWithdrawalsPage.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
changeIdentity: vi.fn(),
|
||||
changeKeyword: vi.fn(),
|
||||
changeRecordType: vi.fn(),
|
||||
changeStatus: vi.fn(),
|
||||
changeTimeRange: vi.fn(),
|
||||
dataTable: vi.fn(),
|
||||
resetFilters: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/host-org/hooks/useHostWithdrawalsPage.js", () => ({
|
||||
useHostWithdrawalsPage: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/ui/DataTable.jsx", () => ({
|
||||
DataTable: (props) => {
|
||||
mocks.dataTable(props);
|
||||
return (
|
||||
<div data-testid="host-withdrawals-table">
|
||||
{props.columns.map((column) => (
|
||||
<div data-testid={`column-${column.key}`} key={column.key}>
|
||||
{column.label}
|
||||
</div>
|
||||
))}
|
||||
{props.items.map((item) => (
|
||||
<div data-testid={`row-${item.id}`} key={item.id}>
|
||||
{props.columns.map((column) => (
|
||||
<div key={column.key}>{column.render ? column.render(item) : item[column.key]}</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/ui/TimeRangeFilter.jsx", () => ({
|
||||
TimeRangeFilter: ({ onChange }) => (
|
||||
<button type="button" onClick={() => onChange({ endMs: "2000", startMs: "1000" })}>
|
||||
时间
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("host withdrawals page renders salary transfer and withdrawal records", () => {
|
||||
vi.mocked(useHostWithdrawalsPage).mockReturnValue(pageFixture());
|
||||
|
||||
render(<HostWithdrawalsPage />);
|
||||
|
||||
expect(screen.getByText("工资转币商")).toBeInTheDocument();
|
||||
expect(screen.getByText("提现申请")).toBeInTheDocument();
|
||||
expect(screen.getByText("Host")).toBeInTheDocument();
|
||||
expect(screen.getByText("BD Leader/Admin")).toBeInTheDocument();
|
||||
expect(screen.getByText("$123.45")).toBeInTheDocument();
|
||||
expect(screen.getByText("1,112")).toBeInTheDocument();
|
||||
expect(screen.getByText("已完成")).toBeInTheDocument();
|
||||
expect(screen.getByText("待审核")).toBeInTheDocument();
|
||||
expect(screen.getByText("USDT-TRC20")).toBeInTheDocument();
|
||||
expect(screen.getByText("TRON9ADDRESS")).toBeInTheDocument();
|
||||
expect(screen.getByText("tx-salary-1")).toBeInTheDocument();
|
||||
expect(screen.getByText("9")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("host withdrawals filters are wired to the page hook", () => {
|
||||
vi.mocked(useHostWithdrawalsPage).mockReturnValue(pageFixture({ hasActiveFilters: true }));
|
||||
|
||||
render(<HostWithdrawalsPage />);
|
||||
|
||||
const props = mocks.dataTable.mock.calls.at(-1)?.[0];
|
||||
props.columns.find((column) => column.key === "recordType").filter.onChange("withdrawal");
|
||||
props.columns.find((column) => column.key === "identity").filter.onChange("bd_leader_admin");
|
||||
props.columns.find((column) => column.key === "sourceUser").filter.onChange("168557");
|
||||
props.columns.find((column) => column.key === "status").filter.onChange("approved");
|
||||
fireEvent.click(screen.getByRole("button", { name: "时间" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "重置" }));
|
||||
|
||||
expect(mocks.changeRecordType).toHaveBeenCalledWith("withdrawal");
|
||||
expect(mocks.changeIdentity).toHaveBeenCalledWith("bd_leader_admin");
|
||||
expect(mocks.changeKeyword).toHaveBeenCalledWith("168557");
|
||||
expect(mocks.changeStatus).toHaveBeenCalledWith("approved");
|
||||
expect(mocks.changeTimeRange).toHaveBeenCalledWith({ endMs: "2000", startMs: "1000" });
|
||||
expect(mocks.resetFilters).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
function pageFixture(patch = {}) {
|
||||
return {
|
||||
abilities: { canView: true },
|
||||
changeIdentity: mocks.changeIdentity,
|
||||
changeKeyword: mocks.changeKeyword,
|
||||
changeRecordType: mocks.changeRecordType,
|
||||
changeStatus: mocks.changeStatus,
|
||||
changeTimeRange: mocks.changeTimeRange,
|
||||
data: {
|
||||
items: [salaryTransferFixture(), withdrawalFixture()],
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
total: 2,
|
||||
},
|
||||
error: null,
|
||||
hasActiveFilters: false,
|
||||
identity: "",
|
||||
keyword: "",
|
||||
loading: false,
|
||||
page: 1,
|
||||
recordType: "",
|
||||
reload: vi.fn(),
|
||||
resetFilters: mocks.resetFilters,
|
||||
setPage: vi.fn(),
|
||||
status: "",
|
||||
timeRange: { endMs: "", startMs: "" },
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
function salaryTransferFixture() {
|
||||
return {
|
||||
coinAmount: 1112,
|
||||
commandId: "cmd-salary-1",
|
||||
createdAtMs: 1780000000000,
|
||||
id: "salary_transfer:101",
|
||||
identity: "host",
|
||||
recordType: "salary_transfer",
|
||||
salaryAssetType: "HOST_SALARY_USD",
|
||||
sellerUser: {
|
||||
displayUserId: "168556",
|
||||
userId: "3001",
|
||||
username: "Coin Seller",
|
||||
},
|
||||
sellerUserId: "3001",
|
||||
sourceUser: {
|
||||
displayUserId: "168577",
|
||||
userId: "1001",
|
||||
username: "Raj",
|
||||
},
|
||||
sourceUserId: "1001",
|
||||
status: "completed",
|
||||
transactionId: "tx-salary-1",
|
||||
updatedAtMs: 1780000000000,
|
||||
usdMinorAmount: 12345,
|
||||
};
|
||||
}
|
||||
|
||||
function withdrawalFixture() {
|
||||
return {
|
||||
applicationId: "9",
|
||||
coinAmount: 0,
|
||||
commandId: "freeze-cmd-9",
|
||||
createdAtMs: 1780000001000,
|
||||
freezeTransactionId: "freeze-tx-9",
|
||||
id: "withdrawal:9",
|
||||
identity: "bd_leader_admin",
|
||||
recordType: "withdrawal",
|
||||
salaryAssetType: "ADMIN_SALARY_USD",
|
||||
sourceUser: {
|
||||
displayUserId: "481479",
|
||||
userId: "2002",
|
||||
username: "Mao",
|
||||
},
|
||||
sourceUserId: "2002",
|
||||
status: "pending",
|
||||
updatedAtMs: 1780000001000,
|
||||
usdMinorAmount: 5000,
|
||||
withdrawAddress: "TRON9ADDRESS",
|
||||
withdrawMethod: "USDT-TRC20",
|
||||
};
|
||||
}
|
||||
@ -67,3 +67,11 @@ export function useCoinSellerAbilities() {
|
||||
canView: can(PERMISSIONS.coinSellerView),
|
||||
};
|
||||
}
|
||||
|
||||
export function useHostWithdrawalAbilities() {
|
||||
const { can } = useAuth();
|
||||
|
||||
return {
|
||||
canView: can(PERMISSIONS.hostWithdrawalView),
|
||||
};
|
||||
}
|
||||
|
||||
@ -65,4 +65,12 @@ export const hostOrgRoutes = [
|
||||
path: "/host/coin-sellers",
|
||||
permission: PERMISSIONS.coinSellerView,
|
||||
},
|
||||
{
|
||||
label: "主播提现",
|
||||
loader: () => import("./pages/HostWithdrawalsPage.jsx").then((module) => module.HostWithdrawalsPage),
|
||||
menuCode: MENU_CODES.hostWithdrawals,
|
||||
pageKey: "host-withdrawals",
|
||||
path: "/host/withdrawals",
|
||||
permission: PERMISSIONS.hostWithdrawalView,
|
||||
},
|
||||
];
|
||||
|
||||
@ -175,6 +175,7 @@ export const API_OPERATIONS = {
|
||||
listHostAgencyPolicies: "listHostAgencyPolicies",
|
||||
listHosts: "listHosts",
|
||||
listHostSalarySettlements: "listHostSalarySettlements",
|
||||
listHostWithdrawals: "listHostWithdrawals",
|
||||
listInviteActivityRewardClaims: "listInviteActivityRewardClaims",
|
||||
listLevelConfig: "listLevelConfig",
|
||||
listLoginLogs: "listLoginLogs",
|
||||
@ -1460,6 +1461,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "host-salary-settlement:view",
|
||||
permissions: ["host-salary-settlement:view"]
|
||||
},
|
||||
listHostWithdrawals: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listHostWithdrawals,
|
||||
path: "/v1/admin/host-withdrawals",
|
||||
permission: "host-withdrawal:view",
|
||||
permissions: ["host-withdrawal:view"]
|
||||
},
|
||||
listInviteActivityRewardClaims: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listInviteActivityRewardClaims,
|
||||
|
||||
94
src/shared/api/generated/schema.d.ts
vendored
94
src/shared/api/generated/schema.d.ts
vendored
@ -1140,6 +1140,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/host-withdrawals": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listHostWithdrawals"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/countries": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -3672,6 +3688,12 @@ export interface components {
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
ApiPageHostWithdrawal: {
|
||||
items: components["schemas"]["HostWithdrawal"][];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
ApiPageRechargeBill: {
|
||||
items: components["schemas"]["RechargeBill"][];
|
||||
page: number;
|
||||
@ -3714,6 +3736,9 @@ export interface components {
|
||||
ApiResponseFinanceWithdrawalApplicationPage: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["ApiPageFinanceWithdrawalApplication"];
|
||||
};
|
||||
ApiResponseHostWithdrawalPage: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["ApiPageHostWithdrawal"];
|
||||
};
|
||||
ApiResponseRechargeBillPage: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["ApiPageRechargeBill"];
|
||||
};
|
||||
@ -3845,6 +3870,44 @@ export interface components {
|
||||
updatedAtMs?: number;
|
||||
userId: string;
|
||||
};
|
||||
HostWithdrawalUser: {
|
||||
userId?: string;
|
||||
displayUserId?: string;
|
||||
username?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
HostWithdrawal: {
|
||||
id: string;
|
||||
/** @enum {string} */
|
||||
recordType: "salary_transfer" | "withdrawal";
|
||||
identity: string;
|
||||
salaryAssetType: string;
|
||||
sourceUserId?: string;
|
||||
sourceUser?: components["schemas"]["HostWithdrawalUser"];
|
||||
sellerUserId?: string;
|
||||
sellerUser?: components["schemas"]["HostWithdrawalUser"];
|
||||
/** Format: int64 */
|
||||
usdMinorAmount: number;
|
||||
/** Format: int64 */
|
||||
coinAmount: number;
|
||||
status: string;
|
||||
transactionId?: string;
|
||||
commandId?: string;
|
||||
applicationId?: string;
|
||||
freezeTransactionId?: string;
|
||||
auditTransactionId?: string;
|
||||
withdrawMethod?: string;
|
||||
withdrawAddress?: string;
|
||||
auditRemark?: string;
|
||||
auditImageUrl?: string;
|
||||
metadata?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
/** Format: int64 */
|
||||
createdAtMs: number;
|
||||
/** Format: int64 */
|
||||
updatedAtMs: number;
|
||||
};
|
||||
HumanRoomRobotCountryRule: {
|
||||
allowedOwnerIds?: string[];
|
||||
countryCode: string;
|
||||
@ -4137,6 +4200,15 @@ export interface components {
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
HostWithdrawalPageResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApiResponseHostWithdrawalPage"];
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
HostProfilePageResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@ -5802,6 +5874,28 @@ export interface operations {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
listHostWithdrawals: {
|
||||
parameters: {
|
||||
query?: {
|
||||
page?: components["parameters"]["Page"];
|
||||
page_size?: components["parameters"]["PageSize"];
|
||||
app_code?: string;
|
||||
record_type?: "salary_transfer" | "withdrawal";
|
||||
identity?: "host" | "agency" | "bd" | "bd_leader_admin";
|
||||
status?: components["parameters"]["Status"];
|
||||
keyword?: components["parameters"]["Keyword"];
|
||||
start_at_ms?: number;
|
||||
end_at_ms?: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["HostWithdrawalPageResponse"];
|
||||
};
|
||||
};
|
||||
listCountries: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@ -273,6 +273,32 @@ export interface CoinSellerLedgerDto {
|
||||
transferUsdMinor?: number;
|
||||
}
|
||||
|
||||
export interface HostWithdrawalDto {
|
||||
applicationId?: string;
|
||||
auditImageUrl?: string;
|
||||
auditRemark?: string;
|
||||
auditTransactionId?: string;
|
||||
coinAmount: number;
|
||||
commandId?: string;
|
||||
createdAtMs?: number;
|
||||
freezeTransactionId?: string;
|
||||
id: string;
|
||||
identity: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
recordType: string;
|
||||
salaryAssetType: string;
|
||||
sellerUser?: CoinLedgerUserDto;
|
||||
sellerUserId?: string;
|
||||
sourceUser?: CoinLedgerUserDto;
|
||||
sourceUserId?: string;
|
||||
status: string;
|
||||
transactionId?: string;
|
||||
updatedAtMs?: number;
|
||||
usdMinorAmount: number;
|
||||
withdrawAddress?: string;
|
||||
withdrawMethod?: string;
|
||||
}
|
||||
|
||||
export interface CoinAdjustmentOperatorDto {
|
||||
adminId?: string;
|
||||
name?: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user