From d8b65c6d82d951cce611070628c273445e1eeff9 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Sun, 24 May 2026 20:25:12 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=8F=90=E7=8E=B0?= =?UTF-8?q?=E7=94=B3=E8=AF=B7=E4=B8=8E=E6=8F=90=E7=8E=B0=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../public/assets/locales/en-US/menu.json | 3 +- .../public/assets/locales/zh-CN/menu.json | 3 +- apps/admin/src/layout/navs.ts | 5 + apps/admin/src/routeTree.gen.ts | 26 ++ .../dashboard/withdrawal/index.lazy.tsx | 6 + apps/admin/src/sections/withdrawal/index.tsx | 302 +++++++++++++++++ .../assets/locales/en-US/affiliate.json | 28 +- .../assets/locales/zh-CN/affiliate.json | 28 +- .../src/sections/user/affiliate/index.tsx | 319 +++++++++++++++++- bun.lock | 4 + packages/ui/src/services/admin/typings.d.ts | 32 ++ packages/ui/src/services/admin/withdrawal.ts | 55 +++ 12 files changed, 790 insertions(+), 21 deletions(-) create mode 100644 apps/admin/src/routes/dashboard/withdrawal/index.lazy.tsx create mode 100644 apps/admin/src/sections/withdrawal/index.tsx create mode 100644 packages/ui/src/services/admin/withdrawal.ts diff --git a/apps/admin/public/assets/locales/en-US/menu.json b/apps/admin/public/assets/locales/en-US/menu.json index db4da07..069ad99 100644 --- a/apps/admin/public/assets/locales/en-US/menu.json +++ b/apps/admin/public/assets/locales/en-US/menu.json @@ -34,5 +34,6 @@ "Traffic Details": "Traffic Details", "Device Group": "Device Group", "User Management": "User Management", - "Users & Support": "Users & Support" + "Users & Support": "Users & Support", + "Withdrawal Management": "Withdrawal Management" } diff --git a/apps/admin/public/assets/locales/zh-CN/menu.json b/apps/admin/public/assets/locales/zh-CN/menu.json index faf2ae8..c711a74 100644 --- a/apps/admin/public/assets/locales/zh-CN/menu.json +++ b/apps/admin/public/assets/locales/zh-CN/menu.json @@ -34,5 +34,6 @@ "Traffic Details": "流量详情", "Device Group": "设备组", "User Management": "用户管理", - "Users & Support": "用户与支持" + "Users & Support": "用户与支持", + "Withdrawal Management": "提现管理" } diff --git a/apps/admin/src/layout/navs.ts b/apps/admin/src/layout/navs.ts index 228d175..5c8bb28 100644 --- a/apps/admin/src/layout/navs.ts +++ b/apps/admin/src/layout/navs.ts @@ -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", diff --git a/apps/admin/src/routeTree.gen.ts b/apps/admin/src/routeTree.gen.ts index eab2f6b..0398349 100644 --- a/apps/admin/src/routeTree.gen.ts +++ b/apps/admin/src/routeTree.gen.ts @@ -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 = diff --git a/apps/admin/src/routes/dashboard/withdrawal/index.lazy.tsx b/apps/admin/src/routes/dashboard/withdrawal/index.lazy.tsx new file mode 100644 index 0000000..842b427 --- /dev/null +++ b/apps/admin/src/routes/dashboard/withdrawal/index.lazy.tsx @@ -0,0 +1,6 @@ +import { createLazyFileRoute } from "@tanstack/react-router"; +import WithdrawalPage from "@/sections/withdrawal"; + +export const Route = createLazyFileRoute("/dashboard/withdrawal/")({ + component: WithdrawalPage, +}); diff --git a/apps/admin/src/sections/withdrawal/index.tsx b/apps/admin/src/sections/withdrawal/index.tsx new file mode 100644 index 0000000..ab4f541 --- /dev/null +++ b/apps/admin/src/sections/withdrawal/index.tsx @@ -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(null); + const [rejectOpen, setRejectOpen] = useState(false); + const [currentRow, setCurrentRow] = useState(null); + const form = useForm>({ + 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 = { + 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 ( + <> + + action={ref} + actions={{ + render: (row) => { + if (row.status !== 0) return []; + + return [ + { + await approveMutation.mutateAsync(row.id); + }} + title="确认通过提现吗?" + trigger={ + + } + />, + , + ]; + }, + }} + columns={[ + { + accessorKey: "id", + header: "ID", + }, + { + accessorKey: "user_id", + header: t("user", "User"), + cell: ({ row }) => , + }, + { + accessorKey: "amount", + header: "提现金额", + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "content", + header: "收款信息", + cell: ({ row }) => ( +
+ {row.original.content || "--"} +
+ ), + }, + { + accessorKey: "status", + header: "状态", + cell: ({ row }) => { + const status = statusMap[row.original.status] ?? { + label: `未知 (${row.original.status})`, + className: "", + }; + return ( + + {status.label} + + ); + }, + }, + { + 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, + }; + }} + /> + + { + setRejectOpen(open); + if (!open) { + setCurrentRow(null); + form.reset({ reason: "" }); + } + }} + open={rejectOpen} + > + + + 拒绝提现申请 + + 为用户填写本次拒绝原因,提交后该记录会更新为已拒绝。 + + +
+ { + if (!currentRow) return; + await rejectMutation.mutateAsync({ + withdrawal_id: currentRow.id, + reason: values.reason.trim(), + }); + })} + > + ( + + 拒绝原因 + +