feat: 佣金提现和备注功能
This commit is contained in:
@@ -46,10 +46,14 @@ import { Icon } from '@workspace/airo-ui/custom-components/icon';
|
||||
import { cn } from '@workspace/airo-ui/lib/utils';
|
||||
import { formatDate } from '@workspace/airo-ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import NextImage from 'next/legacy/image';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { PhotoProvider, PhotoView } from 'react-photo-view';
|
||||
import 'react-photo-view/dist/react-photo-view.css';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('ticket');
|
||||
|
||||
@@ -178,13 +182,13 @@ export default function Page() {
|
||||
<CardDescription className='flex gap-2'>
|
||||
{item.status !== 4 ? (
|
||||
<>
|
||||
<AiroButton
|
||||
variant={'primary'}
|
||||
onClick={() => setTicketId(item.id)}
|
||||
className={'hidden sm:flex'}
|
||||
>
|
||||
{t('reply')}
|
||||
</AiroButton>
|
||||
{item.issue_type === 0 ? (
|
||||
<AiroButton
|
||||
variant={'primary'}
|
||||
onClick={() => setTicketId(item.id)}
|
||||
className={'hidden sm:flex'}
|
||||
/>
|
||||
) : null}
|
||||
<ConfirmButton
|
||||
key='close'
|
||||
trigger={
|
||||
@@ -228,7 +232,28 @@ export default function Page() {
|
||||
</li>
|
||||
<li className='order-2 sm:order-3'>
|
||||
<span className='font-normal text-[#225BA9]'>{t('description')}</span>
|
||||
<time className={'font-bold'}>{item.description}</time>
|
||||
<time className={'font-bold'}>
|
||||
{item?.description?.includes('data:image') ? (
|
||||
<div>
|
||||
<div>提现方式:{item?.description?.split('-')[0]}</div>
|
||||
<PhotoProvider>
|
||||
<PhotoView src={item?.description?.split('-')[1]}>
|
||||
<Image
|
||||
src={item?.description?.split('-')[1]}
|
||||
height={48}
|
||||
width={48}
|
||||
className={'mx-1 cursor-pointer border'}
|
||||
/>
|
||||
</PhotoView>
|
||||
</PhotoProvider>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div>提现方式:{item?.description.split('-')[0]}</div>
|
||||
<div>提现地址: {item?.description.split('-')[1]}</div>
|
||||
</div>
|
||||
)}
|
||||
</time>
|
||||
</li>
|
||||
<li className=''>
|
||||
<span className='font-normal text-[#225BA9]'>{t('updatedAt')}</span>
|
||||
@@ -305,6 +330,7 @@ export default function Page() {
|
||||
from: 'User',
|
||||
type: 1,
|
||||
content: message,
|
||||
issue_type: 0,
|
||||
});
|
||||
refetchTicket();
|
||||
setMessage('');
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { QueryClient, QueryClientProvider, useMutation } from '@tanstack/react-query';
|
||||
import { AiroButton } from '@workspace/airo-ui/components/AiroButton';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@workspace/airo-ui/components/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from '@workspace/airo-ui/components/form';
|
||||
import { Input } from '@workspace/airo-ui/components/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@workspace/airo-ui/components/select';
|
||||
import { Icon } from '@workspace/airo-ui/custom-components/icon';
|
||||
import { UploadImage } from '@workspace/airo-ui/custom-components/upload-image';
|
||||
import { FormLabel } from '@workspace/ui/components/form';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
// 创建一个 QueryClient 实例
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
// 引入实际的 createUserTicket 服务
|
||||
import Modal from '@/components/Modal';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { createUserTicket, getUserTicketList } from '@/services/user/ticket';
|
||||
import { EnhancedInput } from '@workspace/airo-ui/custom-components/enhanced-input';
|
||||
import { Upload } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface WalletDialogProps {
|
||||
commission: number;
|
||||
}
|
||||
|
||||
const WalletDialog: WalletDialogProps = (props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pendingData, setPendingData] = useState(null);
|
||||
const ModalRef = useRef(null);
|
||||
const ErrorModalRef = useRef(null);
|
||||
|
||||
const { common } = useGlobalStore();
|
||||
const { currency } = common;
|
||||
|
||||
// 定义支持的账户类型
|
||||
const ACCOUNT_TYPE = ['USDT', '微信', '支付宝'] as const;
|
||||
|
||||
// 根据账户类型定义 Zod 验证模式
|
||||
const formSchema = z
|
||||
.object({
|
||||
type: z.enum(ACCOUNT_TYPE, {
|
||||
required_error: '请选择一个提现方式',
|
||||
}),
|
||||
account: z.string().optional(), // 账号字段变为可选
|
||||
money: z
|
||||
.string()
|
||||
.min(1, '提现金额不能为空')
|
||||
.regex(/^\d+(\.\d+)?$/, '请输入有效的金额')
|
||||
.refine((value) => {
|
||||
const amount = parseFloat(value);
|
||||
return !isNaN(amount) && amount > 0;
|
||||
}, '提现金额必须大于0'),
|
||||
avatar: z.string().optional(), // 图片字段变为可选
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
// 根据提现方式进行条件验证
|
||||
if (data.type === 'USDT') {
|
||||
if (!data.account || data.account.trim().length === 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'USDT 账号不能为空',
|
||||
path: ['account'],
|
||||
});
|
||||
}
|
||||
} else if (data.type === '微信' || data.type === '支付宝') {
|
||||
if (!data.avatar) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: '请上传图片',
|
||||
path: ['avatar'],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
type: ACCOUNT_TYPE[0],
|
||||
account: '',
|
||||
money: '',
|
||||
avatar: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// 使用 useMutation 来处理表单提交
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (data) => {
|
||||
// 构建 title 和 description
|
||||
const title = `提现金额-${data.money}`;
|
||||
let description = '';
|
||||
if (data.type === 'USDT') {
|
||||
description = `${data.type}-${data.account || ''}`;
|
||||
} else if (data.type === '微信' || data.type === '支付宝') {
|
||||
description = `${data.type}-${data.avatar || ''}`;
|
||||
}
|
||||
return createUserTicket({ title, description, issue_type: 1 });
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
console.log('提交成功:', data);
|
||||
toast.success('提交成功,请耐心等待');
|
||||
// 成功后关闭弹窗并重置表单
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
setPendingData(null);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('提交失败:', error);
|
||||
// 可以在这里显示一个错误提示
|
||||
},
|
||||
});
|
||||
|
||||
const currentType = form.watch('type');
|
||||
const money = form.watch('money');
|
||||
const account = form.watch('account');
|
||||
|
||||
// 处理表单提交,先展示确认弹窗或错误弹窗
|
||||
const handleFormSubmit = async (data) => {
|
||||
// 检查提现佣金和当前佣金,如果超过做提示
|
||||
const moneyValue = parseFloat(data.money);
|
||||
if (moneyValue > parseFloat((props.commission / 100).toFixed(2))) {
|
||||
toast.error('提现金额超过佣金总额');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有待处理的工单,如果超过一个则返回
|
||||
const { data: ticketData } = await getUserTicketList({
|
||||
page: 1,
|
||||
size: 1,
|
||||
issue_type: 1,
|
||||
});
|
||||
if (ticketData?.list?.length > 1) {
|
||||
toast.info('已经存在待处理提现,请耐心等待');
|
||||
return;
|
||||
}
|
||||
|
||||
if (moneyValue < 200) {
|
||||
if (ErrorModalRef.current) {
|
||||
ErrorModalRef.current.show();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 将数据存储到 state 中,供确认弹窗使用
|
||||
setPendingData(data);
|
||||
if (ModalRef.current) {
|
||||
ModalRef.current.show();
|
||||
}
|
||||
};
|
||||
|
||||
// 弹窗确认后执行的提交逻辑
|
||||
const handleModalConfirm = () => {
|
||||
if (pendingData) {
|
||||
mutation.mutate(pendingData);
|
||||
}
|
||||
};
|
||||
|
||||
const loading = mutation.isPending;
|
||||
|
||||
// 根据提现方式动态生成弹窗描述
|
||||
const getModalDescription = () => {
|
||||
if (currentType === 'USDT') {
|
||||
const accountInfo = account || '未知地址';
|
||||
return `请确认您的提现地址及金额无误,您将提现${money}RMB至${accountInfo}地址账号。`;
|
||||
} else {
|
||||
return `请确认您的收款码及金额无误,您将提现${money}RMB至对应收款码账户。`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(newOpen) => {
|
||||
setOpen(newOpen);
|
||||
if (!newOpen) {
|
||||
form.reset();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<AiroButton
|
||||
variant={'link'}
|
||||
className={'min-w-0 px-1 text-sm font-light text-[#225BA9] hover:no-underline'}
|
||||
>
|
||||
提现
|
||||
</AiroButton>
|
||||
</DialogTrigger>
|
||||
<DialogContent className='sm:w-[675px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle className='text-left text-2xl'>佣金提现</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className={'pt-4'}>
|
||||
<div className={'pb-2 text-sm font-semibold text-[#7A7A7A]'}>
|
||||
{currentType === 'USDT'
|
||||
? '将佣金提现至您的个人数字钱包,无手续费'
|
||||
: '该提现方式需10%手续费,该费率由支付平台收取'}
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleFormSubmit)} className=''>
|
||||
{/* 提现方式选择 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='type'
|
||||
render={({ field }) => (
|
||||
<FormItem className={'mb-5'}>
|
||||
<FormLabel className=''>提现方式</FormLabel>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<SelectTrigger
|
||||
className={
|
||||
'h-[46px] rounded-full border-4 border-[#225BA9] bg-[#B5C9E2] px-6 focus:ring-0'
|
||||
}
|
||||
>
|
||||
<SelectValue placeholder={'提现方式'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACCOUNT_TYPE.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 提现账号输入(仅当选择USDT时显示) */}
|
||||
{currentType === 'USDT' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='account'
|
||||
render={({ field }) => (
|
||||
<FormItem className={'mb-2'}>
|
||||
<FormLabel className=''>提现地址</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className={
|
||||
'h-[46px] rounded-full px-6 shadow-[inset_0_0_7.6px_0_#00000040]'
|
||||
}
|
||||
placeholder='提现地址'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 提现金额输入 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='money'
|
||||
render={({ field }) => (
|
||||
<FormItem className={'mb-2'}>
|
||||
<FormLabel className=''>提现金额</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
placeholder='不小于200RMB'
|
||||
min={0}
|
||||
value={field.value}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(String(value));
|
||||
}}
|
||||
prefix={currency.currency_symbol}
|
||||
suffix={currency.currency_unit}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 图片上传(仅当选择微信或支付宝时显示) */}
|
||||
{(currentType === '微信' || currentType === '支付宝') && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='avatar'
|
||||
render={({ field }) => (
|
||||
<FormItem className={'mb-2'}>
|
||||
<FormLabel className=''>收款码</FormLabel>
|
||||
<FormControl>
|
||||
<UploadImage
|
||||
className={`flex h-[46px] items-center justify-center rounded-full border-2 bg-[#EAEAEA] p-4 px-2 text-sm text-[#225BA9] ${field.value ? 'border-[#225BA9]' : 'border-[#EAEAEA]'}`}
|
||||
returnType='base64'
|
||||
onChange={(value) => field.onChange(value)}
|
||||
>
|
||||
点击上传收款码
|
||||
<Upload size={14} className={'ml-3 font-semibold'} />
|
||||
</UploadImage>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 按钮区域 */}
|
||||
<div className='mt-6 flex justify-center gap-2'>
|
||||
<AiroButton
|
||||
type='submit'
|
||||
variant='primary'
|
||||
disabled={loading}
|
||||
className='min-w-[100px]'
|
||||
>
|
||||
{loading && <Icon icon='mdi:loading' className='animate-spin' />}
|
||||
确定
|
||||
</AiroButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Modal
|
||||
ref={ModalRef}
|
||||
title={'重要提示'}
|
||||
wrapClassName={'w-90'}
|
||||
descriptionClassName={'font-normal text-[#4D4D4D]'}
|
||||
description={getModalDescription()}
|
||||
onConfirm={handleModalConfirm}
|
||||
confirmText={'确认'}
|
||||
cancelText={'取消'}
|
||||
/>
|
||||
<Modal
|
||||
ref={ErrorModalRef}
|
||||
title={'重要提示'}
|
||||
wrapClassName={'w-80'}
|
||||
footerClassName={'hidden'}
|
||||
descriptionClassName={'font-normal text-[#4D4D4D]'}
|
||||
description={'提现仅支持不小于200RMB金额,请重新填写合适金额后再进行提现。'}
|
||||
onConfirm={() => {}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// 导出一个包装了 QueryClientProvider 的组件,以确保 useMutation 可用
|
||||
const WrappedWalletDialog = (props) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<WalletDialog {...props} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
export default WrappedWalletDialog;
|
||||
@@ -10,6 +10,7 @@ import Recharge from '@/components/subscribe/recharge';
|
||||
import Link from 'next/link';
|
||||
import Table from './components/Table/Table';
|
||||
import WalletDialog from './components/WalletDialog/WalletDialog';
|
||||
import WhithdrawDialog from './components/Withdraw/WithdrawDialog';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('wallet');
|
||||
@@ -50,7 +51,10 @@ export default function Page() {
|
||||
</p>
|
||||
</div>
|
||||
<div className='rounded-[20px] bg-[#EAEAEA] p-4 shadow-sm transition-all duration-300 hover:shadow-md'>
|
||||
<p className='text-sm font-light text-[#666] opacity-80 sm:mb-3'>{t('commission')}</p>
|
||||
<p className='flex items-center justify-between text-sm font-light text-[#666] opacity-80 sm:mb-3'>
|
||||
<span>{t('commission')}</span>
|
||||
<WhithdrawDialog commission={user?.commission} />
|
||||
</p>
|
||||
<p className='text-xl font-medium text-[#225BA9]'>
|
||||
<Display type='currency' value={user?.commission} />
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user