This commit is contained in:
@@ -93,6 +93,11 @@ export function useNavs() {
|
||||
url: "/dashboard/user",
|
||||
icon: "flat-color-icons:conference-call",
|
||||
},
|
||||
{
|
||||
title: t("Withdrawal Management", "Withdrawal Management"),
|
||||
url: "/dashboard/withdrawal",
|
||||
icon: "flat-color-icons:money-transfer",
|
||||
},
|
||||
{
|
||||
title: t("Device Group", "Device Group"),
|
||||
url: "/dashboard/family",
|
||||
|
||||
@@ -17,6 +17,9 @@ const IndexLazyRouteImport = createFileRoute('/')()
|
||||
const DashboardIndexLazyRouteImport = createFileRoute('/dashboard/')()
|
||||
const DashboardServersLazyRouteImport = createFileRoute('/dashboard/servers')()
|
||||
const DashboardNodesLazyRouteImport = createFileRoute('/dashboard/nodes')()
|
||||
const DashboardWithdrawalIndexLazyRouteImport = createFileRoute(
|
||||
'/dashboard/withdrawal/',
|
||||
)()
|
||||
const DashboardUserIndexLazyRouteImport = createFileRoute('/dashboard/user/')()
|
||||
const DashboardTicketIndexLazyRouteImport =
|
||||
createFileRoute('/dashboard/ticket/')()
|
||||
@@ -123,6 +126,14 @@ const DashboardNodesLazyRoute = DashboardNodesLazyRouteImport.update({
|
||||
} as any).lazy(() =>
|
||||
import('./routes/dashboard/nodes.lazy').then((d) => d.Route),
|
||||
)
|
||||
const DashboardWithdrawalIndexLazyRoute =
|
||||
DashboardWithdrawalIndexLazyRouteImport.update({
|
||||
id: '/withdrawal/',
|
||||
path: '/withdrawal/',
|
||||
getParentRoute: () => DashboardRouteLazyRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/dashboard/withdrawal/index.lazy').then((d) => d.Route),
|
||||
)
|
||||
const DashboardUserIndexLazyRoute = DashboardUserIndexLazyRouteImport.update({
|
||||
id: '/user/',
|
||||
path: '/user/',
|
||||
@@ -375,6 +386,7 @@ export interface FileRoutesByFullPath {
|
||||
'/dashboard/system': typeof DashboardSystemIndexLazyRoute
|
||||
'/dashboard/ticket': typeof DashboardTicketIndexLazyRoute
|
||||
'/dashboard/user': typeof DashboardUserIndexLazyRoute
|
||||
'/dashboard/withdrawal': typeof DashboardWithdrawalIndexLazyRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexLazyRoute
|
||||
@@ -409,6 +421,7 @@ export interface FileRoutesByTo {
|
||||
'/dashboard/system': typeof DashboardSystemIndexLazyRoute
|
||||
'/dashboard/ticket': typeof DashboardTicketIndexLazyRoute
|
||||
'/dashboard/user': typeof DashboardUserIndexLazyRoute
|
||||
'/dashboard/withdrawal': typeof DashboardWithdrawalIndexLazyRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
@@ -445,6 +458,7 @@ export interface FileRoutesById {
|
||||
'/dashboard/system/': typeof DashboardSystemIndexLazyRoute
|
||||
'/dashboard/ticket/': typeof DashboardTicketIndexLazyRoute
|
||||
'/dashboard/user/': typeof DashboardUserIndexLazyRoute
|
||||
'/dashboard/withdrawal/': typeof DashboardWithdrawalIndexLazyRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
@@ -482,6 +496,7 @@ export interface FileRouteTypes {
|
||||
| '/dashboard/system'
|
||||
| '/dashboard/ticket'
|
||||
| '/dashboard/user'
|
||||
| '/dashboard/withdrawal'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
@@ -516,6 +531,7 @@ export interface FileRouteTypes {
|
||||
| '/dashboard/system'
|
||||
| '/dashboard/ticket'
|
||||
| '/dashboard/user'
|
||||
| '/dashboard/withdrawal'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
@@ -551,6 +567,7 @@ export interface FileRouteTypes {
|
||||
| '/dashboard/system/'
|
||||
| '/dashboard/ticket/'
|
||||
| '/dashboard/user/'
|
||||
| '/dashboard/withdrawal/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
@@ -595,6 +612,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof DashboardNodesLazyRouteImport
|
||||
parentRoute: typeof DashboardRouteLazyRoute
|
||||
}
|
||||
'/dashboard/withdrawal/': {
|
||||
id: '/dashboard/withdrawal/'
|
||||
path: '/withdrawal'
|
||||
fullPath: '/dashboard/withdrawal'
|
||||
preLoaderRoute: typeof DashboardWithdrawalIndexLazyRouteImport
|
||||
parentRoute: typeof DashboardRouteLazyRoute
|
||||
}
|
||||
'/dashboard/user/': {
|
||||
id: '/dashboard/user/'
|
||||
path: '/user'
|
||||
@@ -826,6 +850,7 @@ interface DashboardRouteLazyRouteChildren {
|
||||
DashboardSystemIndexLazyRoute: typeof DashboardSystemIndexLazyRoute
|
||||
DashboardTicketIndexLazyRoute: typeof DashboardTicketIndexLazyRoute
|
||||
DashboardUserIndexLazyRoute: typeof DashboardUserIndexLazyRoute
|
||||
DashboardWithdrawalIndexLazyRoute: typeof DashboardWithdrawalIndexLazyRoute
|
||||
}
|
||||
|
||||
const DashboardRouteLazyRouteChildren: DashboardRouteLazyRouteChildren = {
|
||||
@@ -860,6 +885,7 @@ const DashboardRouteLazyRouteChildren: DashboardRouteLazyRouteChildren = {
|
||||
DashboardSystemIndexLazyRoute: DashboardSystemIndexLazyRoute,
|
||||
DashboardTicketIndexLazyRoute: DashboardTicketIndexLazyRoute,
|
||||
DashboardUserIndexLazyRoute: DashboardUserIndexLazyRoute,
|
||||
DashboardWithdrawalIndexLazyRoute: DashboardWithdrawalIndexLazyRoute,
|
||||
}
|
||||
|
||||
const DashboardRouteLazyRouteWithChildren =
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import WithdrawalPage from "@/sections/withdrawal";
|
||||
|
||||
export const Route = createLazyFileRoute("/dashboard/withdrawal/")({
|
||||
component: WithdrawalPage,
|
||||
});
|
||||
@@ -0,0 +1,302 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Badge } from "@workspace/ui/components/badge";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@workspace/ui/components/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@workspace/ui/components/form";
|
||||
import { Textarea } from "@workspace/ui/components/textarea";
|
||||
import { ConfirmButton } from "@workspace/ui/composed/confirm-button";
|
||||
import {
|
||||
ProTable,
|
||||
type ProTableActions,
|
||||
} from "@workspace/ui/composed/pro-table/pro-table";
|
||||
import {
|
||||
approveWithdrawal,
|
||||
getWithdrawalList,
|
||||
rejectWithdrawal,
|
||||
} from "@workspace/ui/services/admin/withdrawal";
|
||||
import { useRef, useState } from "react";
|
||||
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 { UserDetail } from "@/sections/user/user-detail";
|
||||
import { formatDate } from "@/utils/common";
|
||||
|
||||
const rejectSchema = z.object({
|
||||
reason: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(2, "请填写拒绝原因")
|
||||
.max(500, "拒绝原因不能超过 500 个字符"),
|
||||
});
|
||||
|
||||
export default function WithdrawalPage() {
|
||||
const { t } = useTranslation("user");
|
||||
const ref = useRef<ProTableActions>(null);
|
||||
const [rejectOpen, setRejectOpen] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<API.WithdrawalLog | null>(null);
|
||||
const form = useForm<z.infer<typeof rejectSchema>>({
|
||||
resolver: zodResolver(rejectSchema),
|
||||
defaultValues: {
|
||||
reason: "",
|
||||
},
|
||||
});
|
||||
|
||||
const approveMutation = useMutation({
|
||||
mutationFn: async (withdrawalId: number) =>
|
||||
approveWithdrawal({ withdrawal_id: withdrawalId }),
|
||||
onSuccess: () => {
|
||||
toast.success("提现申请已通过");
|
||||
ref.current?.refresh();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Approve withdrawal failed", error);
|
||||
toast.error("提现通过失败");
|
||||
},
|
||||
});
|
||||
|
||||
const rejectMutation = useMutation({
|
||||
mutationFn: async (values: { withdrawal_id: number; reason: string }) =>
|
||||
rejectWithdrawal(values),
|
||||
onSuccess: () => {
|
||||
toast.success("提现申请已拒绝");
|
||||
setRejectOpen(false);
|
||||
setCurrentRow(null);
|
||||
form.reset({ reason: "" });
|
||||
ref.current?.refresh();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Reject withdrawal failed", error);
|
||||
toast.error("提现拒绝失败");
|
||||
},
|
||||
});
|
||||
|
||||
const statusOptions = [
|
||||
{ value: "0", label: "审核中" },
|
||||
{ value: "1", label: "已通过" },
|
||||
{ value: "2", label: "已拒绝" },
|
||||
];
|
||||
|
||||
const statusMap: Record<number, { label: string; className: string }> = {
|
||||
0: {
|
||||
label: "审核中",
|
||||
className: "bg-amber-100 text-amber-700 border-amber-200",
|
||||
},
|
||||
1: {
|
||||
label: "已通过",
|
||||
className: "bg-emerald-100 text-emerald-700 border-emerald-200",
|
||||
},
|
||||
2: {
|
||||
label: "已拒绝",
|
||||
className: "bg-rose-100 text-rose-700 border-rose-200",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProTable<API.WithdrawalLog, { status?: string; user_id?: string }>
|
||||
action={ref}
|
||||
actions={{
|
||||
render: (row) => {
|
||||
if (row.status !== 0) return [];
|
||||
|
||||
return [
|
||||
<ConfirmButton
|
||||
cancelText={t("cancel", "Cancel")}
|
||||
confirmText={t("confirm", "Confirm")}
|
||||
description="确认后将标记该提现为已通过,请确保线下打款流程已完成。"
|
||||
key="approve"
|
||||
onConfirm={async () => {
|
||||
await approveMutation.mutateAsync(row.id);
|
||||
}}
|
||||
title="确认通过提现吗?"
|
||||
trigger={
|
||||
<Button
|
||||
disabled={approveMutation.isPending}
|
||||
size="sm"
|
||||
variant="default"
|
||||
>
|
||||
通过
|
||||
</Button>
|
||||
}
|
||||
/>,
|
||||
<Button
|
||||
key="reject"
|
||||
onClick={() => {
|
||||
setCurrentRow(row);
|
||||
setRejectOpen(true);
|
||||
}}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
>
|
||||
拒绝
|
||||
</Button>,
|
||||
];
|
||||
},
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "user_id",
|
||||
header: t("user", "User"),
|
||||
cell: ({ row }) => <UserDetail id={row.original.user_id} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "amount",
|
||||
header: "提现金额",
|
||||
cell: ({ row }) => (
|
||||
<Display type="currency" value={row.original.amount} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "content",
|
||||
header: "收款信息",
|
||||
cell: ({ row }) => (
|
||||
<div className="max-w-[360px] break-all text-sm">
|
||||
{row.original.content || "--"}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "状态",
|
||||
cell: ({ row }) => {
|
||||
const status = statusMap[row.original.status] ?? {
|
||||
label: `未知 (${row.original.status})`,
|
||||
className: "",
|
||||
};
|
||||
return (
|
||||
<Badge className={status.className} variant="outline">
|
||||
{status.label}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "reason",
|
||||
header: "拒绝原因",
|
||||
cell: ({ row }) => row.original.reason || "--",
|
||||
},
|
||||
{
|
||||
accessorKey: "created_at",
|
||||
header: "申请时间",
|
||||
cell: ({ row }) => formatDate(row.original.created_at),
|
||||
},
|
||||
{
|
||||
accessorKey: "updated_at",
|
||||
header: "更新时间",
|
||||
cell: ({ row }) => formatDate(row.original.updated_at),
|
||||
},
|
||||
]}
|
||||
header={{ title: "提现管理" }}
|
||||
params={[
|
||||
{
|
||||
key: "status",
|
||||
options: statusOptions,
|
||||
placeholder: "状态",
|
||||
},
|
||||
{
|
||||
key: "user_id",
|
||||
placeholder: "用户 ID",
|
||||
},
|
||||
]}
|
||||
request={async (pagination, filter) => {
|
||||
const { data } = await getWithdrawalList({
|
||||
page: pagination.page,
|
||||
size: pagination.size,
|
||||
status: filter.status ? Number(filter.status) : undefined,
|
||||
user_id: filter.user_id ? Number(filter.user_id) : undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
onOpenChange={(open) => {
|
||||
setRejectOpen(open);
|
||||
if (!open) {
|
||||
setCurrentRow(null);
|
||||
form.reset({ reason: "" });
|
||||
}
|
||||
}}
|
||||
open={rejectOpen}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>拒绝提现申请</DialogTitle>
|
||||
<DialogDescription>
|
||||
为用户填写本次拒绝原因,提交后该记录会更新为已拒绝。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
if (!currentRow) return;
|
||||
await rejectMutation.mutateAsync({
|
||||
withdrawal_id: currentRow.id,
|
||||
reason: values.reason.trim(),
|
||||
});
|
||||
})}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="reason"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>拒绝原因</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="请输入拒绝原因,用户端会看到这条说明"
|
||||
rows={5}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => setRejectOpen(false)}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{t("cancel", "Cancel")}
|
||||
</Button>
|
||||
<Button disabled={rejectMutation.isPending} type="submit">
|
||||
{rejectMutation.isPending ? "提交中..." : "确认拒绝"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user