From d1697bed75fcb6ccb03a9b4effdd350b6f7f9d32 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Tue, 26 May 2026 08:37:03 -0700 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD(#43):=20=E6=8F=90?= =?UTF-8?q?=E7=8E=B0=E5=89=8D=E7=AB=AF=E4=BC=98=E5=8C=96=20=E2=80=94=20?= =?UTF-8?q?=E6=94=B6=E6=AC=BE=E6=96=B9=E5=BC=8F=E9=80=89=E6=8B=A9=20+=20?= =?UTF-8?q?=E6=94=B6=E6=AC=BE=E7=A0=81=E4=B8=8A=E4=BC=A0=20+=20=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/sections/withdrawal/index.tsx | 89 +++- .../src/sections/user/affiliate/index.tsx | 393 +++++++++++++++--- packages/ui/src/services/admin/typings.d.ts | 6 +- packages/ui/src/services/user/typings.d.ts | 14 +- packages/ui/src/services/user/user.ts | 34 ++ 5 files changed, 468 insertions(+), 68 deletions(-) diff --git a/apps/admin/src/sections/withdrawal/index.tsx b/apps/admin/src/sections/withdrawal/index.tsx index ab4f541..ef7be0f 100644 --- a/apps/admin/src/sections/withdrawal/index.tsx +++ b/apps/admin/src/sections/withdrawal/index.tsx @@ -40,6 +40,13 @@ import { Display } from "@/components/display"; import { UserDetail } from "@/sections/user/user-detail"; import { formatDate } from "@/utils/common"; +const WITHDRAWAL_METHOD_MAP: Record = { + 0: "其他", + 1: "支付宝", + 2: "微信", + 3: "银行卡", +}; + const rejectSchema = z.object({ reason: z .string() @@ -48,11 +55,40 @@ const rejectSchema = z.object({ .max(500, "拒绝原因不能超过 500 个字符"), }); +function QrCodeCell({ url }: { url?: string }) { + const [zoomOpen, setZoomOpen] = useState(false); + if (!url) return --; + return ( + <> + + {zoomOpen && ( + + + 收款码 + + + )} + + ); +} + export default function WithdrawalPage() { const { t } = useTranslation("user"); const ref = useRef(null); const [rejectOpen, setRejectOpen] = useState(false); - const [currentRow, setCurrentRow] = useState(null); + const [currentRow, setCurrentRow] = useState( + null + ); const form = useForm>({ resolver: zodResolver(rejectSchema), defaultValues: { @@ -93,6 +129,14 @@ export default function WithdrawalPage() { { value: "0", label: "审核中" }, { value: "1", label: "已通过" }, { value: "2", label: "已拒绝" }, + { value: "3", label: "已取消" }, + ]; + + const methodOptions = [ + { value: "1", label: "支付宝" }, + { value: "2", label: "微信" }, + { value: "3", label: "银行卡" }, + { value: "0", label: "其他" }, ]; const statusMap: Record = { @@ -108,11 +152,18 @@ export default function WithdrawalPage() { label: "已拒绝", className: "bg-rose-100 text-rose-700 border-rose-200", }, + 3: { + label: "已取消", + className: "bg-gray-100 text-gray-500 border-gray-200", + }, }; return ( <> - + action={ref} actions={{ render: (row) => { @@ -170,10 +221,32 @@ export default function WithdrawalPage() { ), }, { - accessorKey: "content", - header: "收款信息", + accessorKey: "method", + header: "收款方式", + cell: ({ row }) => + WITHDRAWAL_METHOD_MAP[row.original.method] ?? "--", + }, + { + accessorKey: "account", + header: "收款账号", cell: ({ row }) => ( -
+
+ {row.original.account || "--"} +
+ ), + }, + { + accessorKey: "qr_code_url", + header: "收款码", + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "content", + header: "备注", + cell: ({ row }) => ( +
{row.original.content || "--"}
), @@ -216,6 +289,11 @@ export default function WithdrawalPage() { options: statusOptions, placeholder: "状态", }, + { + key: "method", + options: methodOptions, + placeholder: "收款方式", + }, { key: "user_id", placeholder: "用户 ID", @@ -226,6 +304,7 @@ export default function WithdrawalPage() { page: pagination.page, size: pagination.size, status: filter.status ? Number(filter.status) : undefined, + method: filter.method ? Number(filter.method) : undefined, user_id: filter.user_id ? Number(filter.user_id) : undefined, }); diff --git a/apps/user/src/sections/user/affiliate/index.tsx b/apps/user/src/sections/user/affiliate/index.tsx index 70b4867..989770d 100644 --- a/apps/user/src/sections/user/affiliate/index.tsx +++ b/apps/user/src/sections/user/affiliate/index.tsx @@ -1,7 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { Button } from "@workspace/ui/components/button"; import { Card, @@ -28,18 +28,29 @@ import { FormMessage, } from "@workspace/ui/components/form"; import { Input } from "@workspace/ui/components/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@workspace/ui/components/select"; import { Textarea } from "@workspace/ui/components/textarea"; +import { ConfirmButton } from "@workspace/ui/composed/confirm-button"; import type { ProListActions } from "@workspace/ui/composed/pro-list/pro-list"; import { ProList } from "@workspace/ui/composed/pro-list/pro-list"; +import { UploadImage } from "@workspace/ui/composed/upload-image"; import { commissionWithdraw, queryUserAffiliate, queryUserAffiliateList, queryWithdrawalLog, + uploadFile, + withdrawalCancel, } from "@workspace/ui/services/user/user"; import { formatDate } from "@workspace/ui/utils/formatting"; import { unitConversion } from "@workspace/ui/utils/unit-conversions"; -import { Copy } from "lucide-react"; +import { Copy, X } from "lucide-react"; import { useRef, useState, useTransition } from "react"; import { CopyToClipboard } from "react-copy-to-clipboard"; import { useForm } from "react-hook-form"; @@ -49,6 +60,17 @@ import { z } from "zod"; import { Display } from "@/components/display"; import { useGlobalStore } from "@/stores/global"; +const WITHDRAWAL_METHODS = [ + { value: 1, label: "支付宝" }, + { value: 2, label: "微信" }, + { value: 3, label: "银行卡" }, + { value: 0, label: "其他" }, +] as const; + +function getWithdrawalMethodLabel(method: number): string { + return WITHDRAWAL_METHODS.find((m) => m.value === method)?.label ?? "未知"; +} + function getWithdrawalStatusMeta( status: number, t: ReturnType>["t"] @@ -64,6 +86,11 @@ function getWithdrawalStatusMeta( label: t("withdrawal.status.rejected", "Rejected"), className: "bg-destructive/10 text-destructive", }; + case 3: + return { + label: t("withdrawal.status.cancelled", "Cancelled"), + className: "bg-muted text-muted-foreground", + }; default: return { label: t("withdrawal.status.pending", "Pending"), @@ -72,11 +99,83 @@ function getWithdrawalStatusMeta( } } +function QrCodeView({ url }: { url: string }) { + const [zoomOpen, setZoomOpen] = useState(false); + return ( + <> + + {zoomOpen && ( + + + 收款码 + + + )} + + ); +} + +const withdrawalFormSchema = z + .object({ + amount: z.number(), + method: z.number(), + account: z.string().trim().max(200).optional(), + qr_code_url: z.string().optional(), + content: z.string().trim().max(1000).optional(), + }) + .superRefine((data, ctx) => { + if (!data.amount || data.amount <= 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "提现金额必须大于 0", + path: ["amount"], + }); + } + if (data.method === 1 || data.method === 2) { + if (!data.qr_code_url) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "请上传收款码", + path: ["qr_code_url"], + }); + } + } else if (data.method === 3) { + if (!data.account) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "银行卡收款账号为必填", + path: ["account"], + }); + } + } else if (data.method === 0) { + if (!data.account && !data.content) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "收款账号和备注至少填一项", + path: ["account"], + }); + } + } + }); + +type WithdrawalFormValues = z.infer; + export default function Affiliate() { const { t } = useTranslation("affiliate"); const { user, common, getUserInfo } = useGlobalStore(); const [withdrawalOpen, setWithdrawalOpen] = useState(false); const [submitting, startTransition] = useTransition(); + const [qrUploading, setQrUploading] = useState(false); const withdrawalListActionRef = useRef(undefined); const affiliateQuery = useQuery({ @@ -89,51 +188,69 @@ export default function Affiliate() { 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>({ + + const withdrawalForm = useForm({ resolver: zodResolver(withdrawalFormSchema), defaultValues: { amount: 0, + method: 1, + account: "", + qr_code_url: "", content: "", }, }); - const submitWithdrawal = (values: z.infer) => { + const watchedMethod = withdrawalForm.watch("method"); + const watchedQrCodeUrl = withdrawalForm.watch("qr_code_url"); + + const handleQrUpload = async (file: string | File) => { + if (!(file instanceof File)) return; + setQrUploading(true); + try { + const formData = new FormData(); + formData.append("file", file); + formData.append("biz_type", "withdrawal_qrcode"); + const response = await uploadFile(formData); + const uploadedUrl = + response.data.data?.url || response.data.data?.key || ""; + withdrawalForm.setValue("qr_code_url", uploadedUrl); + withdrawalForm.clearErrors("qr_code_url"); + } catch { + toast.error("收款码上传失败,请重试"); + } finally { + setQrUploading(false); + } + }; + + const cancelMutation = useMutation({ + mutationFn: async (withdrawalId: number) => + withdrawalCancel({ withdrawal_id: withdrawalId }), + onSuccess: () => { + toast.success("提现申请已取消"); + withdrawalListActionRef.current?.refresh(); + }, + onError: () => { + toast.error("取消失败,请重试"); + }, + }); + + const submitWithdrawal = (values: WithdrawalFormValues) => { + if (values.amount > availableCommission) { + withdrawalForm.setError("amount", { + message: t( + "withdrawal.validation.amountExceeded", + "Amount cannot exceed available commission" + ), + }); + return; + } startTransition(async () => { await commissionWithdraw({ amount: values.amount, - content: values.content.trim(), + method: values.method, + account: values.account || undefined, + qr_code_url: values.qr_code_url || undefined, + content: values.content || undefined, }); toast.success( t("withdrawal.submitSuccess", "Withdrawal request submitted") @@ -146,6 +263,9 @@ export default function Affiliate() { setWithdrawalOpen(false); withdrawalForm.reset({ amount: 0, + method: 1, + account: "", + qr_code_url: "", content: "", }); }); @@ -192,7 +312,7 @@ export default function Affiliate() { {t("withdrawal.apply", "Apply for Withdrawal")} - + {t("withdrawal.applyTitle", "Apply for Withdrawal")} @@ -256,21 +376,130 @@ export default function Affiliate() { )} /> + + ( + + 收款方式 + + + + )} + /> + + {(watchedMethod === 1 || watchedMethod === 2) && ( + ( + + + 收款码 + + * + + + {watchedQrCodeUrl ? ( +
+ + +
+ ) : ( + +
+ {qrUploading ? "上传中..." : "点击上传\n收款码"} +
+
+ )} + +
+ )} + /> + )} + + {(watchedMethod === 3 || watchedMethod === 0) && ( + ( + + + 收款账号 + {watchedMethod === 3 && ( + + * + + )} + + + + + + + )} + /> + )} + ( - - {t("withdrawal.content", "Payout Information")} - + 备注