feat: add withdrawal management pages
This commit is contained in:
@@ -15,6 +15,9 @@ import { Route as rootRouteImport } from './routes/__root'
|
||||
const DashboardRouteLazyRouteImport = createFileRoute('/dashboard')()
|
||||
const IndexLazyRouteImport = createFileRoute('/')()
|
||||
const DashboardIndexLazyRouteImport = createFileRoute('/dashboard/')()
|
||||
const DashboardWithdrawalLazyRouteImport = createFileRoute(
|
||||
'/dashboard/withdrawal',
|
||||
)()
|
||||
const DashboardServersLazyRouteImport = createFileRoute('/dashboard/servers')()
|
||||
const DashboardNodesLazyRouteImport = createFileRoute('/dashboard/nodes')()
|
||||
const DashboardWithdrawalIndexLazyRouteImport = createFileRoute(
|
||||
@@ -112,6 +115,13 @@ const DashboardIndexLazyRoute = DashboardIndexLazyRouteImport.update({
|
||||
} as any).lazy(() =>
|
||||
import('./routes/dashboard/index.lazy').then((d) => d.Route),
|
||||
)
|
||||
const DashboardWithdrawalLazyRoute = DashboardWithdrawalLazyRouteImport.update({
|
||||
id: '/withdrawal',
|
||||
path: '/withdrawal',
|
||||
getParentRoute: () => DashboardRouteLazyRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/dashboard/withdrawal.lazy').then((d) => d.Route),
|
||||
)
|
||||
const DashboardServersLazyRoute = DashboardServersLazyRouteImport.update({
|
||||
id: '/servers',
|
||||
path: '/servers',
|
||||
@@ -357,6 +367,7 @@ export interface FileRoutesByFullPath {
|
||||
'/dashboard': typeof DashboardRouteLazyRouteWithChildren
|
||||
'/dashboard/nodes': typeof DashboardNodesLazyRoute
|
||||
'/dashboard/servers': typeof DashboardServersLazyRoute
|
||||
'/dashboard/withdrawal': typeof DashboardWithdrawalLazyRoute
|
||||
'/dashboard/': typeof DashboardIndexLazyRoute
|
||||
'/dashboard/log/balance': typeof DashboardLogBalanceLazyRoute
|
||||
'/dashboard/log/commission': typeof DashboardLogCommissionLazyRoute
|
||||
@@ -392,6 +403,7 @@ export interface FileRoutesByTo {
|
||||
'/': typeof IndexLazyRoute
|
||||
'/dashboard/nodes': typeof DashboardNodesLazyRoute
|
||||
'/dashboard/servers': typeof DashboardServersLazyRoute
|
||||
'/dashboard/withdrawal': typeof DashboardWithdrawalLazyRoute
|
||||
'/dashboard': typeof DashboardIndexLazyRoute
|
||||
'/dashboard/log/balance': typeof DashboardLogBalanceLazyRoute
|
||||
'/dashboard/log/commission': typeof DashboardLogCommissionLazyRoute
|
||||
@@ -429,6 +441,7 @@ export interface FileRoutesById {
|
||||
'/dashboard': typeof DashboardRouteLazyRouteWithChildren
|
||||
'/dashboard/nodes': typeof DashboardNodesLazyRoute
|
||||
'/dashboard/servers': typeof DashboardServersLazyRoute
|
||||
'/dashboard/withdrawal': typeof DashboardWithdrawalLazyRoute
|
||||
'/dashboard/': typeof DashboardIndexLazyRoute
|
||||
'/dashboard/log/balance': typeof DashboardLogBalanceLazyRoute
|
||||
'/dashboard/log/commission': typeof DashboardLogCommissionLazyRoute
|
||||
@@ -467,6 +480,7 @@ export interface FileRouteTypes {
|
||||
| '/dashboard'
|
||||
| '/dashboard/nodes'
|
||||
| '/dashboard/servers'
|
||||
| '/dashboard/withdrawal'
|
||||
| '/dashboard/'
|
||||
| '/dashboard/log/balance'
|
||||
| '/dashboard/log/commission'
|
||||
@@ -502,6 +516,7 @@ export interface FileRouteTypes {
|
||||
| '/'
|
||||
| '/dashboard/nodes'
|
||||
| '/dashboard/servers'
|
||||
| '/dashboard/withdrawal'
|
||||
| '/dashboard'
|
||||
| '/dashboard/log/balance'
|
||||
| '/dashboard/log/commission'
|
||||
@@ -538,6 +553,7 @@ export interface FileRouteTypes {
|
||||
| '/dashboard'
|
||||
| '/dashboard/nodes'
|
||||
| '/dashboard/servers'
|
||||
| '/dashboard/withdrawal'
|
||||
| '/dashboard/'
|
||||
| '/dashboard/log/balance'
|
||||
| '/dashboard/log/commission'
|
||||
@@ -598,6 +614,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof DashboardIndexLazyRouteImport
|
||||
parentRoute: typeof DashboardRouteLazyRoute
|
||||
}
|
||||
'/dashboard/withdrawal': {
|
||||
id: '/dashboard/withdrawal'
|
||||
path: '/withdrawal'
|
||||
fullPath: '/dashboard/withdrawal'
|
||||
preLoaderRoute: typeof DashboardWithdrawalLazyRouteImport
|
||||
parentRoute: typeof DashboardRouteLazyRoute
|
||||
}
|
||||
'/dashboard/servers': {
|
||||
id: '/dashboard/servers'
|
||||
path: '/servers'
|
||||
@@ -821,6 +844,7 @@ declare module '@tanstack/react-router' {
|
||||
interface DashboardRouteLazyRouteChildren {
|
||||
DashboardNodesLazyRoute: typeof DashboardNodesLazyRoute
|
||||
DashboardServersLazyRoute: typeof DashboardServersLazyRoute
|
||||
DashboardWithdrawalLazyRoute: typeof DashboardWithdrawalLazyRoute
|
||||
DashboardIndexLazyRoute: typeof DashboardIndexLazyRoute
|
||||
DashboardLogBalanceLazyRoute: typeof DashboardLogBalanceLazyRoute
|
||||
DashboardLogCommissionLazyRoute: typeof DashboardLogCommissionLazyRoute
|
||||
@@ -856,6 +880,7 @@ interface DashboardRouteLazyRouteChildren {
|
||||
const DashboardRouteLazyRouteChildren: DashboardRouteLazyRouteChildren = {
|
||||
DashboardNodesLazyRoute: DashboardNodesLazyRoute,
|
||||
DashboardServersLazyRoute: DashboardServersLazyRoute,
|
||||
DashboardWithdrawalLazyRoute: DashboardWithdrawalLazyRoute,
|
||||
DashboardIndexLazyRoute: DashboardIndexLazyRoute,
|
||||
DashboardLogBalanceLazyRoute: DashboardLogBalanceLazyRoute,
|
||||
DashboardLogCommissionLazyRoute: DashboardLogCommissionLazyRoute,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import WithdrawalManagementPage from "@/sections/withdrawal";
|
||||
|
||||
export const Route = createLazyFileRoute("/dashboard/withdrawal")({
|
||||
component: WithdrawalManagementPage,
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Badge } from "@workspace/ui/components/badge";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -17,9 +17,19 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@workspace/ui/components/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@workspace/ui/components/form";
|
||||
import { Input } from "@workspace/ui/components/input";
|
||||
import { Textarea } from "@workspace/ui/components/textarea";
|
||||
import type { ProListActions } from "@workspace/ui/composed/pro-list/pro-list";
|
||||
import { ProList } from "@workspace/ui/composed/pro-list/pro-list";
|
||||
import {
|
||||
commissionWithdraw,
|
||||
@@ -28,248 +38,268 @@ import {
|
||||
queryWithdrawalLog,
|
||||
} from "@workspace/ui/services/user/user";
|
||||
import { formatDate } from "@workspace/ui/utils/formatting";
|
||||
import { Copy, Wallet } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { unitConversion } from "@workspace/ui/utils/unit-conversions";
|
||||
import { Copy } from "lucide-react";
|
||||
import { useRef, useState, useTransition } from "react";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Display } from "@/components/display";
|
||||
import { useGlobalStore } from "@/stores/global";
|
||||
|
||||
function getWithdrawalStatusMeta(
|
||||
status: number,
|
||||
t: ReturnType<typeof useTranslation<"affiliate">>["t"]
|
||||
) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return {
|
||||
label: t("withdrawal.status.approved", "Approved"),
|
||||
className: "bg-emerald-500/10 text-emerald-600",
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
label: t("withdrawal.status.rejected", "Rejected"),
|
||||
className: "bg-destructive/10 text-destructive",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: t("withdrawal.status.pending", "Pending"),
|
||||
className: "bg-amber-500/10 text-amber-600",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function Affiliate() {
|
||||
const { t } = useTranslation("affiliate");
|
||||
const { user, common, getUserInfo } = useGlobalStore();
|
||||
const queryClient = useQueryClient();
|
||||
const [withdrawOpen, setWithdrawOpen] = useState(false);
|
||||
const [amountInput, setAmountInput] = useState("");
|
||||
const [contentInput, setContentInput] = useState("");
|
||||
const [amountError, setAmountError] = useState("");
|
||||
const [contentError, setContentError] = useState("");
|
||||
const { data } = useQuery({
|
||||
const [withdrawalOpen, setWithdrawalOpen] = useState(false);
|
||||
const [submitting, startTransition] = useTransition();
|
||||
const withdrawalListActionRef = useRef<ProListActions | undefined>(undefined);
|
||||
|
||||
const affiliateQuery = useQuery({
|
||||
queryKey: ["queryUserAffiliate"],
|
||||
queryFn: async () => {
|
||||
const response = await queryUserAffiliate();
|
||||
return response.data.data;
|
||||
},
|
||||
});
|
||||
const withdrawMutation = useMutation({
|
||||
mutationFn: async (values: { amount: number; content: string }) =>
|
||||
commissionWithdraw({
|
||||
amount: Math.round(values.amount * 100),
|
||||
content: values.content.trim(),
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
toast.success(t("withdrawSuccess", "提现申请已提交"));
|
||||
setAmountInput("");
|
||||
setContentInput("");
|
||||
setAmountError("");
|
||||
setContentError("");
|
||||
setWithdrawOpen(false);
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["queryUserAffiliate"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["queryUserInfo"] }),
|
||||
getUserInfo(),
|
||||
]);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Withdraw request failed", error);
|
||||
toast.error(t("withdrawError", "提现申请提交失败"));
|
||||
const availableCommission = Number(
|
||||
user?.commission ?? affiliateQuery.data?.total_commission ?? 0
|
||||
);
|
||||
const withdrawalFormSchema = z.object({
|
||||
amount: z
|
||||
.number()
|
||||
.int(t("withdrawal.validation.amountInvalid", "Amount is invalid"))
|
||||
.positive(
|
||||
t(
|
||||
"withdrawal.validation.amountPositive",
|
||||
"Amount must be greater than 0"
|
||||
)
|
||||
)
|
||||
.max(
|
||||
availableCommission,
|
||||
t(
|
||||
"withdrawal.validation.amountExceeded",
|
||||
"Amount cannot exceed available commission"
|
||||
)
|
||||
),
|
||||
content: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(
|
||||
1,
|
||||
t(
|
||||
"withdrawal.validation.contentRequired",
|
||||
"Withdrawal info is required"
|
||||
)
|
||||
)
|
||||
.max(
|
||||
1000,
|
||||
t("withdrawal.validation.contentTooLong", "Withdrawal info is too long")
|
||||
),
|
||||
});
|
||||
const withdrawalForm = useForm<z.infer<typeof withdrawalFormSchema>>({
|
||||
resolver: zodResolver(withdrawalFormSchema),
|
||||
defaultValues: {
|
||||
amount: 0,
|
||||
content: "",
|
||||
},
|
||||
});
|
||||
|
||||
const statusMap: Record<number, { label: string; className: string }> = {
|
||||
0: {
|
||||
label: t("withdrawStatus.pending", "审核中"),
|
||||
className: "bg-amber-100 text-amber-700 border-amber-200",
|
||||
},
|
||||
1: {
|
||||
label: t("withdrawStatus.approved", "已通过"),
|
||||
className: "bg-emerald-100 text-emerald-700 border-emerald-200",
|
||||
},
|
||||
2: {
|
||||
label: t("withdrawStatus.rejected", "已拒绝"),
|
||||
className: "bg-rose-100 text-rose-700 border-rose-200",
|
||||
},
|
||||
};
|
||||
|
||||
const currentCommission = user?.commission ?? 0;
|
||||
const totalCommission = data?.total_commission ?? 0;
|
||||
|
||||
const submitWithdraw = async () => {
|
||||
const amount = Number(amountInput);
|
||||
const content = contentInput.trim();
|
||||
let hasError = false;
|
||||
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
setAmountError(t("amountPositive", "提现金额必须大于 0"));
|
||||
hasError = true;
|
||||
} else {
|
||||
setAmountError("");
|
||||
}
|
||||
|
||||
if (content.length < 2) {
|
||||
setContentError(t("accountRequired", "请填写收款信息"));
|
||||
hasError = true;
|
||||
} else if (content.length > 200) {
|
||||
setContentError(t("accountTooLong", "收款信息不能超过 200 个字符"));
|
||||
hasError = true;
|
||||
} else {
|
||||
setContentError("");
|
||||
}
|
||||
|
||||
if (hasError) return;
|
||||
|
||||
await withdrawMutation.mutateAsync({
|
||||
amount,
|
||||
content,
|
||||
const submitWithdrawal = (values: z.infer<typeof withdrawalFormSchema>) => {
|
||||
startTransition(async () => {
|
||||
await commissionWithdraw({
|
||||
amount: values.amount,
|
||||
content: values.content.trim(),
|
||||
});
|
||||
toast.success(
|
||||
t("withdrawal.submitSuccess", "Withdrawal request submitted")
|
||||
);
|
||||
await Promise.all([
|
||||
affiliateQuery.refetch(),
|
||||
getUserInfo(),
|
||||
withdrawalListActionRef.current?.refresh(),
|
||||
]);
|
||||
setWithdrawalOpen(false);
|
||||
withdrawalForm.reset({
|
||||
amount: 0,
|
||||
content: "",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<CardTitle>{t("totalCommission", "Total Commission")}</CardTitle>
|
||||
<CardDescription>
|
||||
{t("commissionInfo", "Commission Info")}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Dialog onOpenChange={setWithdrawOpen} open={withdrawOpen}>
|
||||
<Button
|
||||
className="gap-2 self-start"
|
||||
onClick={() => setWithdrawOpen(true)}
|
||||
variant="default"
|
||||
>
|
||||
<Wallet className="h-4 w-4" />
|
||||
{t("applyWithdraw", "申请提现")}
|
||||
</Button>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("applyWithdraw", "申请提现")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t(
|
||||
"withdrawDescription",
|
||||
"填写提现金额和收款信息后提交申请,等待后台审核。"
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-3 rounded-lg border bg-muted/30 p-4 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
{t("currentCommission", "当前可见佣金")}
|
||||
</span>
|
||||
<span className="font-semibold">
|
||||
<Display type="currency" value={currentCommission} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
{t("totalCommission", "Total Commission")}
|
||||
</span>
|
||||
<span className="font-semibold">
|
||||
<Display type="currency" value={totalCommission} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
className="font-medium text-sm"
|
||||
htmlFor="withdraw-amount"
|
||||
>
|
||||
{t("withdrawAmount", "提现金额")}
|
||||
</label>
|
||||
<Input
|
||||
id="withdraw-amount"
|
||||
min="0"
|
||||
onChange={(event) => {
|
||||
setAmountInput(event.target.value);
|
||||
if (amountError) setAmountError("");
|
||||
}}
|
||||
placeholder={t(
|
||||
"withdrawAmountPlaceholder",
|
||||
"请输入提现金额,单位为元"
|
||||
)}
|
||||
step="0.01"
|
||||
type="number"
|
||||
value={amountInput}
|
||||
/>
|
||||
{amountError ? (
|
||||
<p className="text-destructive text-sm">{amountError}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
className="font-medium text-sm"
|
||||
htmlFor="withdraw-content"
|
||||
>
|
||||
{t("accountInfo", "收款信息")}
|
||||
</label>
|
||||
<Textarea
|
||||
id="withdraw-content"
|
||||
onChange={(event) => {
|
||||
setContentInput(event.target.value);
|
||||
if (contentError) setContentError("");
|
||||
}}
|
||||
placeholder={t(
|
||||
"accountPlaceholder",
|
||||
"请输入银行卡、支付宝、USDT 地址等收款信息"
|
||||
)}
|
||||
rows={4}
|
||||
value={contentInput}
|
||||
/>
|
||||
{contentError ? (
|
||||
<p className="text-destructive text-sm">{contentError}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => setWithdrawOpen(false)}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{t("cancel", "取消")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={withdrawMutation.isPending}
|
||||
onClick={submitWithdraw}
|
||||
type="button"
|
||||
>
|
||||
{withdrawMutation.isPending
|
||||
? t("submitting", "提交中...")
|
||||
: t("submitWithdraw", "提交申请")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("totalCommission", "Total Commission")}</CardTitle>
|
||||
<CardDescription>
|
||||
{t("commissionInfo", "Commission Info")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t("totalCommission", "Total Commission")}
|
||||
</div>
|
||||
<div className="mt-1 flex items-baseline gap-2">
|
||||
<span className="font-bold text-3xl">
|
||||
<Display type="currency" value={totalCommission} />
|
||||
</span>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
({t("commissionRate", "Commission Rate")}:{" "}
|
||||
{user?.referral_percentage ||
|
||||
common?.invite?.referral_percentage}
|
||||
%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="font-bold text-3xl">
|
||||
<Display
|
||||
type="currency"
|
||||
value={affiliateQuery.data?.total_commission}
|
||||
/>
|
||||
</span>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
({t("commissionRate", "Commission Rate")}:{" "}
|
||||
{user?.referral_percentage ||
|
||||
common?.invite?.referral_percentage}
|
||||
%)
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex flex-col items-start gap-2 md:items-end">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t("currentCommission", "当前可见佣金")}
|
||||
{t(
|
||||
"withdrawal.availableCommission",
|
||||
"Available for withdrawal"
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 font-bold text-3xl">
|
||||
<Display type="currency" value={currentCommission} />
|
||||
<div className="font-semibold text-xl">
|
||||
<Display type="currency" value={availableCommission} />
|
||||
</div>
|
||||
<Dialog onOpenChange={setWithdrawalOpen} open={withdrawalOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
{t("withdrawal.apply", "Apply for Withdrawal")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t("withdrawal.applyTitle", "Apply for Withdrawal")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t(
|
||||
"withdrawal.applyDescription",
|
||||
"Enter the withdrawal amount and payout details."
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...withdrawalForm}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
id="withdrawal-form"
|
||||
onSubmit={withdrawalForm.handleSubmit(submitWithdrawal)}
|
||||
>
|
||||
<FormField
|
||||
control={withdrawalForm.control}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("withdrawal.amount", "Amount")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
inputMode="decimal"
|
||||
min="0"
|
||||
onChange={(event) => {
|
||||
const cents = unitConversion(
|
||||
"dollarsToCents",
|
||||
Number(event.target.value || 0)
|
||||
);
|
||||
field.onChange(Number(cents) || 0);
|
||||
}}
|
||||
placeholder={t(
|
||||
"withdrawal.amountPlaceholder",
|
||||
"Enter withdrawal amount"
|
||||
)}
|
||||
step="0.01"
|
||||
type="number"
|
||||
value={
|
||||
field.value
|
||||
? unitConversion(
|
||||
"centsToDollars",
|
||||
Number(field.value)
|
||||
)
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("withdrawal.amountHint", "Available")}:{" "}
|
||||
<Display
|
||||
type="currency"
|
||||
value={availableCommission}
|
||||
/>
|
||||
</p>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={withdrawalForm.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("withdrawal.content", "Payout Information")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={t(
|
||||
"withdrawal.contentPlaceholder",
|
||||
"Bank account, wallet address, recipient name, or other payout details"
|
||||
)}
|
||||
rows={4}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
disabled={submitting}
|
||||
onClick={() => setWithdrawalOpen(false)}
|
||||
variant="outline"
|
||||
>
|
||||
{t("withdrawal.cancel", "Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={submitting || availableCommission <= 0}
|
||||
form="withdrawal-form"
|
||||
type="submit"
|
||||
>
|
||||
{submitting
|
||||
? t("withdrawal.submitting", "Submitting...")
|
||||
: t("withdrawal.submit", "Submit Withdrawal")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -302,55 +332,50 @@ export default function Affiliate() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ProList<API.WithdrawalLog, Record<string, unknown>>
|
||||
action={withdrawalListActionRef}
|
||||
header={{
|
||||
title: t("withdrawRecords", "提现记录"),
|
||||
title: t("withdrawal.records", "Withdrawal Records"),
|
||||
}}
|
||||
renderItem={(item) => {
|
||||
const status = statusMap[item.status] ?? {
|
||||
label: `${t("unknownStatus", "未知状态")} (${item.status})`,
|
||||
className: "",
|
||||
};
|
||||
const statusMeta = getWithdrawalStatusMeta(item.status, t);
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="space-y-3 p-4 text-sm">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="text-muted-foreground">
|
||||
{t("withdrawAmount", "提现金额")}
|
||||
</div>
|
||||
<div className="font-semibold text-lg">
|
||||
<Display type="currency" value={item.amount} />
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="font-semibold text-lg">
|
||||
<Display type="currency" value={item.amount} />
|
||||
</div>
|
||||
<Badge className={status.className} variant="outline">
|
||||
{status.label}
|
||||
</Badge>
|
||||
<span
|
||||
className={`inline-flex rounded-full px-3 py-1 font-medium text-xs ${statusMeta.className}`}
|
||||
>
|
||||
{statusMeta.label}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="grid gap-3 md:grid-cols-2">
|
||||
<li className="flex flex-col gap-1">
|
||||
<span className="text-muted-foreground">
|
||||
{t("accountInfo", "收款信息")}
|
||||
{t("withdrawal.record.content", "Payout Information")}
|
||||
</span>
|
||||
<span className="break-all font-medium">
|
||||
{item.content || "--"}
|
||||
<span className="whitespace-pre-wrap break-words">
|
||||
{item.content}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex flex-col gap-1">
|
||||
<span className="text-muted-foreground">
|
||||
{t("applyTime", "申请时间")}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{formatDate(item.created_at)}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex flex-col gap-1 md:col-span-2">
|
||||
<span className="text-muted-foreground">
|
||||
{t("rejectReason", "拒绝原因")}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{item.reason || t("noReason", "无")}
|
||||
{t("withdrawal.record.createdAt", "Created At")}
|
||||
</span>
|
||||
<time>{formatDate(item.created_at)}</time>
|
||||
</li>
|
||||
{item.reason ? (
|
||||
<li className="flex flex-col gap-1 md:col-span-2">
|
||||
<span className="text-muted-foreground">
|
||||
{t("withdrawal.record.reason", "Reject Reason")}
|
||||
</span>
|
||||
<span className="whitespace-pre-wrap break-words text-destructive">
|
||||
{item.reason}
|
||||
</span>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user