420 lines
15 KiB
TypeScript
420 lines
15 KiB
TypeScript
'use client';
|
|
|
|
import useGlobalStore from '@/config/use-global';
|
|
import { getPaymentPlatform } from '@/services/admin/payment';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { Button } from '@workspace/ui/components/button';
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from '@workspace/ui/components/form';
|
|
import { RadioGroup, RadioGroupItem } from '@workspace/ui/components/radio-group';
|
|
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@workspace/ui/components/select';
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetFooter,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
SheetTrigger,
|
|
} from '@workspace/ui/components/sheet';
|
|
import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
|
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
|
import { unitConversion } from '@workspace/ui/utils';
|
|
import { useTranslations } from 'next-intl';
|
|
import { useEffect, useState } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import * as z from 'zod';
|
|
|
|
interface PaymentFormProps<T> {
|
|
trigger: React.ReactNode;
|
|
title: string;
|
|
loading?: boolean;
|
|
initialValues?: T;
|
|
onSubmit: (values: T) => Promise<boolean>;
|
|
isEdit?: boolean;
|
|
}
|
|
|
|
export default function PaymentForm<T>({
|
|
trigger,
|
|
title,
|
|
loading,
|
|
initialValues,
|
|
onSubmit,
|
|
isEdit,
|
|
}: PaymentFormProps<T>) {
|
|
const t = useTranslations('payment');
|
|
const { common } = useGlobalStore();
|
|
const { currency } = common;
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const { data: platformData } = useQuery({
|
|
queryKey: ['getPaymentPlatform'],
|
|
queryFn: async () => {
|
|
const { data } = await getPaymentPlatform();
|
|
return data?.data?.list || [];
|
|
},
|
|
});
|
|
|
|
const formSchema = z.object({
|
|
name: z.string().min(1, { message: t('nameRequired') }),
|
|
platform: z.string().optional(),
|
|
icon: z.string().optional(),
|
|
domain: z.string().optional(),
|
|
config: z.any(),
|
|
fee_mode: z.coerce.number().min(0).max(2),
|
|
fee_percent: z.coerce.number().optional(),
|
|
fee_amount: z.coerce.number().optional(),
|
|
description: z.string().optional(),
|
|
});
|
|
|
|
const form = useForm<z.infer<typeof formSchema>>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: {
|
|
name: '',
|
|
platform: '',
|
|
icon: '',
|
|
domain: '',
|
|
config: {},
|
|
fee_mode: 0,
|
|
fee_percent: 0,
|
|
fee_amount: 0,
|
|
...(initialValues as any),
|
|
},
|
|
});
|
|
|
|
const feeMode = form.watch('fee_mode');
|
|
const platformValue = form.watch('platform');
|
|
const configValues = form.watch('config');
|
|
|
|
const currentPlatform = platformData?.find((p) => p.platform === platformValue);
|
|
const currentFieldDescriptions = currentPlatform?.platform_field_description || {};
|
|
const configFields = Object.keys(currentFieldDescriptions) || [];
|
|
const platformUrl = currentPlatform?.platform_url || '';
|
|
|
|
useEffect(() => {
|
|
if (feeMode === 0) {
|
|
form.setValue('fee_amount', 0);
|
|
form.setValue('fee_percent', 0);
|
|
} else if (feeMode === 1) {
|
|
form.setValue('fee_amount', 0);
|
|
} else if (feeMode === 2) {
|
|
form.setValue('fee_percent', 0);
|
|
}
|
|
}, [feeMode, form]);
|
|
|
|
const handleClose = () => {
|
|
form.reset();
|
|
setOpen(false);
|
|
};
|
|
|
|
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
|
|
const cleanedValues = { ...values };
|
|
|
|
if (values.fee_mode === 0) {
|
|
cleanedValues.fee_amount = undefined;
|
|
cleanedValues.fee_percent = undefined;
|
|
} else if (values.fee_mode === 1) {
|
|
cleanedValues.fee_amount = undefined;
|
|
} else if (values.fee_mode === 2) {
|
|
cleanedValues.fee_percent = undefined;
|
|
}
|
|
|
|
const success = await onSubmit(cleanedValues as unknown as T);
|
|
if (success) {
|
|
handleClose();
|
|
}
|
|
};
|
|
|
|
const openPlatformUrl = () => {
|
|
if (platformUrl) {
|
|
window.open(platformUrl, '_blank');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
<SheetTrigger asChild>{trigger}</SheetTrigger>
|
|
<SheetContent className='w-[550px] max-w-full md:max-w-screen-md'>
|
|
<SheetHeader>
|
|
<SheetTitle>{title}</SheetTitle>
|
|
</SheetHeader>
|
|
<ScrollArea className='-mx-6 h-[calc(100vh-48px-36px-36px-env(safe-area-inset-top))]'>
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-6 px-6 pt-4'>
|
|
{/* 基本信息分组 */}
|
|
<div className='space-y-4'>
|
|
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
|
|
<FormField
|
|
control={form.control}
|
|
name='name'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('name')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
placeholder={t('namePlaceholder')}
|
|
value={field.value}
|
|
onValueChange={(value) => form.setValue('name', value as string)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name='icon'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('icon')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
placeholder={t('iconPlaceholder')}
|
|
value={field.value}
|
|
onValueChange={(value) => form.setValue('icon', value as string)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
<FormField
|
|
control={form.control}
|
|
name='domain'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('domain')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
placeholder={t('domainPlaceholder', { example: 'http(s)://example.com' })}
|
|
value={field.value}
|
|
onValueChange={(value) => form.setValue('domain', value as string)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className='space-y-4'>
|
|
<FormField
|
|
control={form.control}
|
|
name='fee_mode'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('handlingFee')}</FormLabel>
|
|
<FormControl>
|
|
<RadioGroup
|
|
onValueChange={(value) => field.onChange(parseInt(value))}
|
|
value={field.value.toString()}
|
|
className='flex flex-wrap gap-4'
|
|
>
|
|
<FormItem className='flex items-center space-x-2'>
|
|
<FormControl>
|
|
<RadioGroupItem value='0' />
|
|
</FormControl>
|
|
<FormLabel className='!mt-0 cursor-pointer'>{t('noFee')}</FormLabel>
|
|
</FormItem>
|
|
<FormItem className='flex items-center space-x-2'>
|
|
<FormControl>
|
|
<RadioGroupItem value='1' />
|
|
</FormControl>
|
|
<FormLabel className='!mt-0 cursor-pointer'>
|
|
{t('percentFee')}
|
|
</FormLabel>
|
|
</FormItem>
|
|
<FormItem className='flex items-center space-x-2'>
|
|
<FormControl>
|
|
<RadioGroupItem value='2' />
|
|
</FormControl>
|
|
<FormLabel className='!mt-0 cursor-pointer'>{t('fixedFee')}</FormLabel>
|
|
</FormItem>
|
|
</RadioGroup>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{feeMode === 1 && (
|
|
<div className='grid grid-cols-1 sm:w-1/2'>
|
|
<FormField
|
|
control={form.control}
|
|
name='fee_percent'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('feePercent')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
type='number'
|
|
step='0.01'
|
|
suffix='%'
|
|
value={field.value}
|
|
onValueChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{feeMode === 2 && (
|
|
<div className='grid grid-cols-1 sm:w-1/2'>
|
|
<FormField
|
|
control={form.control}
|
|
name='fee_amount'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('feeAmount')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
type='number'
|
|
step='0.01'
|
|
prefix={currency.currency_symbol}
|
|
suffix={currency.currency_unit}
|
|
value={unitConversion('centsToDollars', field.value)}
|
|
onValueChange={(value) =>
|
|
field.onChange(unitConversion('dollarsToCents', value))
|
|
}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className='space-y-4'>
|
|
{(!platformValue || platformData?.find((p) => p.platform === platformValue)) && (
|
|
<FormField
|
|
control={form.control}
|
|
name='platform'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('platform')}</FormLabel>
|
|
<Select
|
|
onValueChange={(value) => {
|
|
form.setValue('platform', value as string);
|
|
form.setValue('config', {});
|
|
}}
|
|
defaultValue={field.value}
|
|
value={field.value}
|
|
// @ts-expect-error
|
|
disabled={isEdit && Boolean(initialValues?.platform)}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('selectPlatform')} />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
{platformData?.map((platform) => (
|
|
<SelectItem key={platform.platform} value={platform.platform}>
|
|
{platform.platform}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{platformUrl ? (
|
|
<div className='mt-1 flex justify-end'>
|
|
<Button
|
|
variant='ghost'
|
|
size='sm'
|
|
className='h-6 px-2 text-xs'
|
|
onClick={openPlatformUrl}
|
|
>
|
|
<Icon icon='tabler:external-link' className='mr-1 h-3 w-3' />
|
|
{t('applyForPayment')}
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className='mt-1 h-6'></div>
|
|
)}
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
{configFields.length > 0 && (
|
|
<div className='mt-4 space-y-4'>
|
|
{configFields.map((fieldKey) => (
|
|
<FormItem key={fieldKey}>
|
|
<FormLabel>{currentFieldDescriptions[fieldKey]}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
placeholder={t('configPlaceholder', {
|
|
field: currentFieldDescriptions[fieldKey],
|
|
})}
|
|
value={
|
|
configValues && configValues[fieldKey] !== undefined
|
|
? configValues[fieldKey]
|
|
: ''
|
|
}
|
|
onValueChange={(value) => {
|
|
const newConfig = { ...configValues };
|
|
newConfig[fieldKey] = value;
|
|
form.setValue('config', newConfig);
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='description'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('description')}</FormLabel>
|
|
<FormControl>
|
|
<MarkdownEditor
|
|
value={field.value}
|
|
onChange={(value) => form.setValue(field.name, value as string)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</ScrollArea>
|
|
|
|
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
|
<Button variant='outline' disabled={loading} onClick={handleClose}>
|
|
{t('cancel')}
|
|
</Button>
|
|
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
|
|
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
|
{t('submit')}
|
|
</Button>
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
}
|