♻️ refactor(payment): Reconstruct the payment page
This commit is contained in:
@@ -1,256 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { getAlipayF2FPaymentConfig, updateAlipayF2FPaymentConfig } from '@/services/admin/payment';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@workspace/ui/components/select';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||
import { Textarea } from '@workspace/ui/components/textarea';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { unitConversion } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function AlipayF2F() {
|
||||
const t = useTranslations('payment');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getAlipayF2FPaymentConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getAlipayF2FPaymentConfig();
|
||||
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateAlipayF2FPaymentConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.UpdateAlipayF2fRequest);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('enable')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('alipayf2f.sandbox')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('alipayf2f.sandboxDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.config.sandbox}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
sandbox: checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('showName')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('showNameDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.name}
|
||||
onValueBlur={(value) => updateConfig('name', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('iconUrl')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('iconUrlDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.icon_url}
|
||||
onValueBlur={(value) => updateConfig('icon', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('notifyUrl')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('notifyUrlDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.domain}
|
||||
onValueBlur={(value) => updateConfig('domain', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('feeMode')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('feeModeDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Select
|
||||
value={String(data?.fee_mode)}
|
||||
onValueChange={(value) => {
|
||||
updateConfig('fee_mode', Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='请选择' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value='0'>{t('feeModeItems.0')}</SelectItem>
|
||||
<SelectItem value='1'>{t('feeModeItems.1')}</SelectItem>
|
||||
<SelectItem value='2'>{t('feeModeItems.2')}</SelectItem>
|
||||
<SelectItem value='3'>{t('feeModeItems.3')}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('feePercent')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('feePercentDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
type='number'
|
||||
min={0}
|
||||
max={100}
|
||||
maxLength={3}
|
||||
value={data?.fee_percent}
|
||||
onValueBlur={(value) => updateConfig('fee_percent', value)}
|
||||
suffix='%'
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('fixedFee')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('fixedFeeDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
min={0}
|
||||
value={data?.fee_amount}
|
||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||
formatOutput={(value) => unitConversion('dollarsToCents', value)}
|
||||
onValueBlur={(value) => updateConfig('fee_amount', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('alipayf2f.appId')}</Label>
|
||||
<p className='text-muted-foreground text-xs' />
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config.app_id}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
app_id: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('alipayf2f.privateKey')}</Label>
|
||||
<p className='text-muted-foreground text-xs' />
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Textarea
|
||||
placeholder={t('inputPlaceholder')}
|
||||
defaultValue={data?.config.private_key}
|
||||
onBlur={(e) => {
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
private_key: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('alipayf2f.publicKey')}</Label>
|
||||
<p className='text-muted-foreground text-xs' />
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Textarea
|
||||
placeholder={t('inputPlaceholder')}
|
||||
defaultValue={data?.config.public_key}
|
||||
onBlur={(e) => {
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
public_key: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('alipayf2f.invoiceName')}</Label>
|
||||
<p className='text-muted-foreground text-xs' />
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('alipayf2f.invoiceNameDescription')}
|
||||
value={data?.config.invoice_name}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
invoice_name: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { getEpayPaymentConfig, updateEpayPaymentConfig } from '@/services/admin/payment';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@workspace/ui/components/select';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { unitConversion } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function Epay() {
|
||||
const t = useTranslations('payment');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getEpayPaymentConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getEpayPaymentConfig();
|
||||
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateEpayPaymentConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.UpdateEpayRequest);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('enable')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('showName')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('showNameDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.name}
|
||||
onValueBlur={(value) => updateConfig('name', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('iconUrl')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('iconUrlDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.icon}
|
||||
onValueBlur={(value) => updateConfig('icon', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('notifyUrl')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('notifyUrlDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.domain}
|
||||
onValueBlur={(value) => updateConfig('domain', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('feeMode')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('feeModeDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Select
|
||||
value={String(data?.fee_mode)}
|
||||
onValueChange={(value) => {
|
||||
updateConfig('fee_mode', Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='请选择' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value='0'>{t('feeModeItems.0')}</SelectItem>
|
||||
<SelectItem value='1'>{t('feeModeItems.1')}</SelectItem>
|
||||
<SelectItem value='2'>{t('feeModeItems.2')}</SelectItem>
|
||||
<SelectItem value='3'>{t('feeModeItems.3')}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('feePercent')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('feePercentDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
type='number'
|
||||
min={0}
|
||||
max={100}
|
||||
maxLength={3}
|
||||
value={data?.fee_percent}
|
||||
onValueBlur={(value) => updateConfig('fee_percent', value)}
|
||||
suffix='%'
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('fixedFee')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('fixedFeeDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
type='number'
|
||||
min={0}
|
||||
value={data?.fee_amount}
|
||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||
formatOutput={(value) => unitConversion('dollarsToCents', value)}
|
||||
onValueBlur={(value) => updateConfig('fee_amount', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('epay.url')}</Label>
|
||||
<p className='text-muted-foreground text-xs' />
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config.url}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
url: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('epay.pid')}</Label>
|
||||
<p className='text-muted-foreground text-xs' />
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config.pid}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
pid: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('epay.key')}</Label>
|
||||
<p className='text-muted-foreground text-xs' />
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config.key}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
key: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +1,11 @@
|
||||
import Billing from '@/components/billing';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import AlipayF2F from './alipayf2f';
|
||||
import Epay from './epay';
|
||||
import StripeAlipay from './stripe-alipay';
|
||||
import StripeWeChatPay from './stripe-wechat-pay';
|
||||
import PaymentTable from './payment-table';
|
||||
|
||||
export default async function Page() {
|
||||
return (
|
||||
<>
|
||||
<Tabs defaultValue='Epay'>
|
||||
<TabsList className='h-full flex-wrap'>
|
||||
<TabsTrigger value='Epay'>Epay</TabsTrigger>
|
||||
<TabsTrigger value='Stripe-Alipay'>Stripe(AliPay)</TabsTrigger>
|
||||
<TabsTrigger value='Strip-WeChatPay'>Stripe(WeChatPay)</TabsTrigger>
|
||||
<TabsTrigger value='AlipayF2F'>AlipayF2F</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='Epay'>
|
||||
<Epay />
|
||||
</TabsContent>
|
||||
<TabsContent value='Stripe-Alipay'>
|
||||
<StripeAlipay />
|
||||
</TabsContent>
|
||||
<TabsContent value='Strip-WeChatPay'>
|
||||
<StripeWeChatPay />
|
||||
</TabsContent>
|
||||
<TabsContent value='AlipayF2F'>
|
||||
<AlipayF2F />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<div className='flex flex-col gap-3'>
|
||||
<PaymentTable />
|
||||
<div className='mt-5 flex flex-col gap-3'>
|
||||
<Billing type='payment' />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
'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 { 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>;
|
||||
}
|
||||
|
||||
export default function PaymentForm<T>({
|
||||
trigger,
|
||||
title,
|
||||
loading,
|
||||
initialValues,
|
||||
onSubmit,
|
||||
}: 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(),
|
||||
});
|
||||
|
||||
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: 'https://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}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
'use client';
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||
import {
|
||||
createPaymentMethod,
|
||||
deletePaymentMethod,
|
||||
getPaymentMethodList,
|
||||
updatePaymentMethod,
|
||||
} from '@/services/admin/payment';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@workspace/ui/components/avatar';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import PaymentForm from './payment-form';
|
||||
|
||||
export default function PaymentTable() {
|
||||
const t = useTranslations('payment');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const ref = useRef<ProTableActions>(null);
|
||||
|
||||
return (
|
||||
<ProTable<API.PaymentConfig, { search: string }>
|
||||
action={ref}
|
||||
header={{
|
||||
title: t('paymentManagement'),
|
||||
toolbar: (
|
||||
<PaymentForm<API.CreatePaymentMethodRequest>
|
||||
trigger={<Button>{t('create')}</Button>}
|
||||
title={t('createPayment')}
|
||||
loading={loading}
|
||||
onSubmit={async (values) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await createPaymentMethod({
|
||||
...values,
|
||||
enable: false,
|
||||
});
|
||||
toast.success(t('createSuccess'));
|
||||
ref.current?.refresh();
|
||||
setLoading(false);
|
||||
return true;
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'enable',
|
||||
header: t('enable'),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Switch
|
||||
checked={Boolean(row.getValue('enable'))}
|
||||
onCheckedChange={async (checked) => {
|
||||
await updatePaymentMethod({
|
||||
...row.original,
|
||||
enable: checked,
|
||||
});
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'icon',
|
||||
header: t('icon'),
|
||||
cell: ({ row }) => {
|
||||
const icon = row.getValue('icon') as string;
|
||||
return (
|
||||
<Avatar className='h-8 w-8'>
|
||||
{icon ? <AvatarImage src={icon} alt={row.getValue('name')} /> : null}
|
||||
<AvatarFallback>
|
||||
{(row.getValue('name') as string)?.charAt(0) || '?'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: t('name'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'platform',
|
||||
header: t('platform'),
|
||||
cell: ({ row }) => <Badge>{t(row.getValue('platform'))}</Badge>,
|
||||
},
|
||||
{
|
||||
accessorKey: 'domain',
|
||||
header: t('domain'),
|
||||
cell: ({ row }) => {
|
||||
const domain = row.getValue('domain') as string;
|
||||
return domain ? <Badge variant='outline'>{domain}</Badge> : '--';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'fee',
|
||||
header: t('handlingFee'),
|
||||
cell: ({ row }) => {
|
||||
const feeMode = row.original.fee_mode;
|
||||
if (feeMode === 1) {
|
||||
return <Badge>{row.original.fee_percent}%</Badge>;
|
||||
} else if (feeMode === 2) {
|
||||
return (
|
||||
<Badge>
|
||||
<Display value={row.original.fee_amount} type='currency' />
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return '--';
|
||||
},
|
||||
},
|
||||
]}
|
||||
params={[
|
||||
{
|
||||
key: 'search',
|
||||
placeholder: t('searchPlaceholder'),
|
||||
},
|
||||
]}
|
||||
request={async (pagination, filter) => {
|
||||
const { data } = await getPaymentMethodList({
|
||||
...pagination,
|
||||
...filter,
|
||||
});
|
||||
return {
|
||||
list: data?.data?.list || [],
|
||||
total: data?.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
actions={{
|
||||
render: (row) => [
|
||||
<PaymentForm<API.UpdatePaymentMethodRequest>
|
||||
key='edit'
|
||||
trigger={<Button>{t('edit')}</Button>}
|
||||
title={t('editPayment')}
|
||||
loading={loading}
|
||||
initialValues={row}
|
||||
onSubmit={async (values) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updatePaymentMethod({
|
||||
...values,
|
||||
id: row.id,
|
||||
});
|
||||
toast.success(t('updateSuccess'));
|
||||
ref.current?.refresh();
|
||||
setLoading(false);
|
||||
return true;
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
<ConfirmButton
|
||||
key='delete'
|
||||
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||
title={t('confirmDelete')}
|
||||
description={t('deleteWarning')}
|
||||
onConfirm={async () => {
|
||||
await deletePaymentMethod({
|
||||
id: row.id,
|
||||
});
|
||||
toast.success(t('deleteSuccess'));
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
cancelText={t('cancel')}
|
||||
confirmText={t('confirm')}
|
||||
/>,
|
||||
<Button
|
||||
key='copy'
|
||||
variant='outline'
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { id, ...params } = row;
|
||||
await createPaymentMethod({
|
||||
...params,
|
||||
enable: false,
|
||||
});
|
||||
toast.success(t('copySuccess'));
|
||||
ref.current?.refresh();
|
||||
setLoading(false);
|
||||
return true;
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('copy')}
|
||||
</Button>,
|
||||
],
|
||||
batchRender(rows) {
|
||||
return [
|
||||
<ConfirmButton
|
||||
key='delete'
|
||||
trigger={<Button variant='destructive'>{t('batchDelete')}</Button>}
|
||||
title={t('confirmDelete')}
|
||||
description={t('deleteWarning')}
|
||||
onConfirm={async () => {
|
||||
for (const row of rows) {
|
||||
await deletePaymentMethod({ id: row.id });
|
||||
}
|
||||
toast.success(t('deleteSuccess'));
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
cancelText={t('cancel')}
|
||||
confirmText={t('confirm')}
|
||||
/>,
|
||||
];
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
getStripeAlipayPaymentConfig,
|
||||
updateStripeAlipayPaymentConfig,
|
||||
} from '@/services/admin/payment';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@workspace/ui/components/select';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { unitConversion } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function Stripe() {
|
||||
const t = useTranslations('payment');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getStripeAlipayPaymentConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getStripeAlipayPaymentConfig();
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateStripeAlipayPaymentConfig({
|
||||
...data,
|
||||
mark: 'stripe_alipay',
|
||||
[key]: value,
|
||||
} as any);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('aliPay')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable}
|
||||
onCheckedChange={async (checked) => {
|
||||
updateConfig('enable', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('showName')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('showNameDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.name}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('name', value);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('iconUrl')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('iconUrlDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.icon}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('icon', value);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('notifyUrl')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('notifyUrlDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.domain}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('domain', value);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('feeMode')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('feeModeDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Select
|
||||
value={String(data?.fee_mode)}
|
||||
onValueChange={(value) => {
|
||||
updateConfig('fee_mode', Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='请选择' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value='0'>{t('feeModeItems.0')}</SelectItem>
|
||||
<SelectItem value='1'>{t('feeModeItems.1')}</SelectItem>
|
||||
<SelectItem value='2'>{t('feeModeItems.2')}</SelectItem>
|
||||
<SelectItem value='3'>{t('feeModeItems.3')}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('feePercent')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('feePercentDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
type='number'
|
||||
min={0}
|
||||
max={100}
|
||||
value={data?.fee_percent}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('fee_percent', value);
|
||||
}}
|
||||
suffix='%'
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('fixedFee')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('fixedFeeDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
type='number'
|
||||
min={0}
|
||||
value={data?.fee_amount}
|
||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||
formatOutput={(value) => unitConversion('dollarsToCents', value)}
|
||||
onValueBlur={(value) => updateConfig('fee_amount', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('stripe.publicKey')}</Label>
|
||||
<p className='text-muted-foreground text-xs' />
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config.public_key}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
public_key: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('stripe.secretKey')}</Label>
|
||||
<p className='text-muted-foreground text-xs' />
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config.secret_key}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
secret_key: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('stripe.webhookSecret')}</Label>
|
||||
<p className='text-muted-foreground text-xs' />
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config.webhook_secret}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
webhook_secret: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
getStripeWeChatPayPaymentConfig,
|
||||
updateStripeWeChatPayPaymentConfig,
|
||||
} from '@/services/admin/payment';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@workspace/ui/components/select';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { unitConversion } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function StripeWeChatPay() {
|
||||
const t = useTranslations('payment');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getStripeWeChatPayPaymentConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getStripeWeChatPayPaymentConfig();
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateStripeWeChatPayPaymentConfig({
|
||||
...data,
|
||||
mark: 'stripe_wechat_pay',
|
||||
[key]: value,
|
||||
} as any);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('wechatPay')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable}
|
||||
onCheckedChange={async (checked) => {
|
||||
updateConfig('enable', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('showName')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('showNameDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.name}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('name', value);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('iconUrl')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('iconUrlDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.icon}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('icon', value);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('notifyUrl')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('notifyUrlDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.domain}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('domain', value);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('feeMode')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('feeModeDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Select
|
||||
value={String(data?.fee_mode)}
|
||||
onValueChange={(value) => {
|
||||
updateConfig('fee_mode', Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='请选择' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value='0'>{t('feeModeItems.0')}</SelectItem>
|
||||
<SelectItem value='1'>{t('feeModeItems.1')}</SelectItem>
|
||||
<SelectItem value='2'>{t('feeModeItems.2')}</SelectItem>
|
||||
<SelectItem value='3'>{t('feeModeItems.3')}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('feePercent')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('feePercentDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
type='number'
|
||||
min={0}
|
||||
max={100}
|
||||
value={data?.fee_percent}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('fee_percent', value);
|
||||
}}
|
||||
suffix='%'
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('fixedFee')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('fixedFeeDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
type='number'
|
||||
min={0}
|
||||
value={data?.fee_amount}
|
||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||
formatOutput={(value) => unitConversion('dollarsToCents', value)}
|
||||
onValueBlur={(value) => updateConfig('fee_amount', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('stripe.publicKey')}</Label>
|
||||
<p className='text-muted-foreground text-xs' />
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config.public_key}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
public_key: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('stripe.secretKey')}</Label>
|
||||
<p className='text-muted-foreground text-xs' />
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config.secret_key}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
secret_key: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('stripe.webhookSecret')}</Label>
|
||||
<p className='text-muted-foreground text-xs' />
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config.webhook_secret}
|
||||
onValueBlur={(value) => {
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
webhook_secret: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user