✨ feat: Add log cleanup settings and update localization files
- Introduced log cleanup settings in the admin panel, allowing configuration of automatic log clearing and retention periods. - Updated English, Spanish, French, German, and other localization files to include new log cleanup settings. - Added new fields for referral percentage and first purchase only in user settings. - Implemented API endpoints for getting and updating log settings. - Enhanced the admin dashboard with a new log cleanup form component.
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
deleteNode,
|
||||
filterNodeList,
|
||||
filterServerList,
|
||||
resetSortWithNode,
|
||||
toggleNodeStatus,
|
||||
updateNode,
|
||||
} from '@/services/admin/server';
|
||||
@@ -235,6 +236,35 @@ export default function NodesPage() {
|
||||
];
|
||||
},
|
||||
}}
|
||||
onSort={async (source, target, items) => {
|
||||
const sourceIndex = items.findIndex((item) => String(item.id) === source);
|
||||
const targetIndex = items.findIndex((item) => String(item.id) === target);
|
||||
|
||||
const originalSorts = items.map((item) => item.sort);
|
||||
|
||||
const [movedItem] = items.splice(sourceIndex, 1);
|
||||
items.splice(targetIndex, 0, movedItem!);
|
||||
|
||||
const updatedItems = items.map((item, index) => {
|
||||
const originalSort = originalSorts[index];
|
||||
const newSort = originalSort !== undefined ? originalSort : item.sort;
|
||||
return { ...item, sort: newSort };
|
||||
});
|
||||
|
||||
const changedItems = updatedItems.filter((item, index) => {
|
||||
return item.sort !== items[index]?.sort;
|
||||
});
|
||||
|
||||
if (changedItems.length > 0) {
|
||||
resetSortWithNode({
|
||||
sort: changedItems.map((item) => ({
|
||||
id: item.id,
|
||||
sort: item.sort,
|
||||
})) as API.SortItem[],
|
||||
});
|
||||
}
|
||||
return updatedItems;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { UserDetail } from '@/app/dashboard/user/user-detail';
|
||||
import { IpLink } from '@/components/ip-link';
|
||||
import { ProTable } from '@/components/pro-table';
|
||||
import { filterServerList } from '@/services/admin/server';
|
||||
import { getUserSubscribeById } from '@/services/admin/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import {
|
||||
@@ -13,55 +13,85 @@ import {
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import type { useTranslations } from 'next-intl';
|
||||
import { formatBytes, formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
|
||||
function mapOnlineUsers(online: API.ServerStatus['online'] = []): {
|
||||
uid: string;
|
||||
ips: string[];
|
||||
subscribe?: string;
|
||||
subscribe_id?: number;
|
||||
traffic?: number;
|
||||
expired_at?: number;
|
||||
}[] {
|
||||
return (online || []).map((u) => ({
|
||||
uid: String(u.user_id || ''),
|
||||
ips: Array.isArray(u.ip) ? u.ip.map(String) : [],
|
||||
subscribe: (u as any).subscribe,
|
||||
subscribe_id: (u as any).subscribe_id,
|
||||
traffic: (u as any).traffic,
|
||||
expired_at: (u as any).expired_at,
|
||||
}));
|
||||
}
|
||||
|
||||
export default function OnlineUsersCell({
|
||||
serverId,
|
||||
status,
|
||||
t,
|
||||
function UserSubscribeInfo({
|
||||
subscribeId,
|
||||
open,
|
||||
type,
|
||||
}: {
|
||||
serverId?: number;
|
||||
status?: API.ServerStatus;
|
||||
t: ReturnType<typeof useTranslations>;
|
||||
subscribeId: number;
|
||||
open: boolean;
|
||||
type: 'account' | 'subscribeName' | 'subscribeId' | 'trafficUsage' | 'expireTime';
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data: latest } = useQuery({
|
||||
queryKey: ['serverStatusById', serverId, open],
|
||||
enabled: !!serverId && open,
|
||||
const t = useTranslations('servers');
|
||||
const { data } = useQuery({
|
||||
enabled: subscribeId !== 0 && open,
|
||||
queryKey: ['getUserSubscribeById', subscribeId],
|
||||
queryFn: async () => {
|
||||
const { data } = await filterServerList({ page: 1, size: 1, search: String(serverId) });
|
||||
const list = (data?.data?.list || []) as API.Server[];
|
||||
return list[0]?.status as API.ServerStatus | undefined;
|
||||
const { data } = await getUserSubscribeById({ id: subscribeId });
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const rows = mapOnlineUsers((latest || status)?.online);
|
||||
const count = rows.length;
|
||||
if (!data) return <span className='text-muted-foreground'>--</span>;
|
||||
|
||||
switch (type) {
|
||||
case 'account':
|
||||
if (!data.user_id) return <span className='text-muted-foreground'>--</span>;
|
||||
return <UserDetail id={data.user_id} />;
|
||||
|
||||
case 'subscribeName':
|
||||
if (!data.subscribe?.name) return <span className='text-muted-foreground'>--</span>;
|
||||
return <span className='text-sm'>{data.subscribe.name}</span>;
|
||||
|
||||
case 'subscribeId':
|
||||
if (!data.id) return <span className='text-muted-foreground'>--</span>;
|
||||
return <span className='font-mono text-sm'>{data.id}</span>;
|
||||
|
||||
case 'trafficUsage': {
|
||||
const usedTraffic = data.upload + data.download;
|
||||
const totalTraffic = data.traffic || 0;
|
||||
return (
|
||||
<div className='min-w-0 text-sm'>
|
||||
<div className='break-words'>
|
||||
{formatBytes(usedTraffic)} / {totalTraffic > 0 ? formatBytes(totalTraffic) : '无限制'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'expireTime': {
|
||||
if (!data.expire_time) return <span className='text-muted-foreground'>--</span>;
|
||||
const isExpired = data.expire_time < Date.now() / 1000;
|
||||
return (
|
||||
<div className='flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-2'>
|
||||
<span className='text-sm'>{formatDate(data.expire_time)}</span>
|
||||
{isExpired && (
|
||||
<Badge variant='destructive' className='w-fit px-1 py-0 text-xs'>
|
||||
{t('expired')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return <span className='text-muted-foreground'>--</span>;
|
||||
}
|
||||
}
|
||||
|
||||
export default function OnlineUsersCell({ status }: { status?: API.ServerStatus }) {
|
||||
const t = useTranslations('servers');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<button className='hover:text-foreground text-muted-foreground flex items-center gap-2 bg-transparent p-0 text-sm'>
|
||||
<Badge variant='secondary'>{count}</Badge>
|
||||
<Badge variant='secondary'>{status?.online.length}</Badge>
|
||||
<span>{t('onlineUsers')}</span>
|
||||
</button>
|
||||
</SheetTrigger>
|
||||
@@ -70,36 +100,20 @@ export default function OnlineUsersCell({
|
||||
<SheetTitle>{t('onlineUsers')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className='-mx-6 h-[calc(100vh-48px-16px)] overflow-y-auto px-6 py-4 sm:h-[calc(100dvh-48px-16px-env(safe-area-inset-top))]'>
|
||||
<ProTable<
|
||||
{
|
||||
uid: string;
|
||||
ips: string[];
|
||||
subscribe?: string;
|
||||
subscribe_id?: number;
|
||||
traffic?: number;
|
||||
expired_at?: number;
|
||||
},
|
||||
Record<string, unknown>
|
||||
>
|
||||
<ProTable<API.ServerOnlineUser, Record<string, unknown>>
|
||||
header={{ hidden: true }}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'ips',
|
||||
accessorKey: 'ip',
|
||||
header: t('ipAddresses'),
|
||||
cell: ({ row }) => {
|
||||
const ips = row.original.ips;
|
||||
const ips = row.original.ip;
|
||||
return (
|
||||
<div className='flex min-w-0 flex-col gap-1'>
|
||||
{ips.map((ip, i) => (
|
||||
<div
|
||||
key={`${row.original.uid}-${ip}`}
|
||||
className='whitespace-nowrap text-sm'
|
||||
>
|
||||
{i === 0 ? (
|
||||
<IpLink ip={ip} className='font-medium' />
|
||||
) : (
|
||||
<IpLink ip={ip} className='text-muted-foreground' />
|
||||
)}
|
||||
{ips.map((item, i) => (
|
||||
<div className='whitespace-nowrap text-sm' key={i}>
|
||||
<Badge>{item.protocol}</Badge>
|
||||
<IpLink ip={item.ip} className='font-medium' />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -109,51 +123,63 @@ export default function OnlineUsersCell({
|
||||
{
|
||||
accessorKey: 'user',
|
||||
header: t('user'),
|
||||
cell: ({ row }) => <UserDetail id={Number(row.original.uid)} />,
|
||||
cell: ({ row }) => (
|
||||
<UserSubscribeInfo
|
||||
subscribeId={Number(row.original.subscribe_id)}
|
||||
open={open}
|
||||
type='account'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'subscription',
|
||||
header: t('subscription'),
|
||||
cell: ({ row }) => (
|
||||
<span className='text-sm'>{row.original.subscribe || '--'}</span>
|
||||
<UserSubscribeInfo
|
||||
subscribeId={Number(row.original.subscribe_id)}
|
||||
open={open}
|
||||
type='subscribeName'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'subscribeId',
|
||||
header: t('subscribeId'),
|
||||
cell: ({ row }) => (
|
||||
<span className='font-mono text-sm'>{row.original.subscribe_id || '--'}</span>
|
||||
<UserSubscribeInfo
|
||||
subscribeId={Number(row.original.subscribe_id)}
|
||||
open={open}
|
||||
type='subscribeId'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'traffic',
|
||||
header: t('traffic'),
|
||||
cell: ({ row }) => {
|
||||
const v = Number(row.original.traffic || 0);
|
||||
return <span className='text-sm'>{(v / 1024 ** 3).toFixed(2)} GB</span>;
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<UserSubscribeInfo
|
||||
subscribeId={Number(row.original.subscribe_id)}
|
||||
open={open}
|
||||
type='trafficUsage'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'expireTime',
|
||||
header: t('expireTime'),
|
||||
cell: ({ row }) => {
|
||||
const ts = Number(row.original.expired_at || 0);
|
||||
if (!ts) return <span className='text-muted-foreground'>--</span>;
|
||||
const expired = ts < Date.now() / 1000;
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='text-sm'>{new Date(ts * 1000).toLocaleString()}</span>
|
||||
{expired && (
|
||||
<Badge variant='destructive' className='w-fit px-1 py-0 text-xs'>
|
||||
{t('expired')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<UserSubscribeInfo
|
||||
subscribeId={Number(row.original.subscribe_id)}
|
||||
open={open}
|
||||
type='expireTime'
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
request={async () => ({ list: rows, total: rows.length })}
|
||||
request={async () => ({
|
||||
list: status?.online || [],
|
||||
total: status?.online?.length || 0,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</SheetContent>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
createServer,
|
||||
deleteServer,
|
||||
filterServerList,
|
||||
resetSortWithServer,
|
||||
updateServer,
|
||||
} from '@/services/admin/server';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
@@ -195,13 +196,7 @@ export default function ServersPage() {
|
||||
{
|
||||
id: 'online_users',
|
||||
header: t('onlineUsers'),
|
||||
cell: ({ row }) => (
|
||||
<OnlineUsersCell
|
||||
serverId={row.original.id}
|
||||
status={row.original.status as API.ServerStatus}
|
||||
t={t}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => <OnlineUsersCell status={row.original.status as API.ServerStatus} />,
|
||||
},
|
||||
{
|
||||
id: 'traffic_ratio',
|
||||
@@ -288,6 +283,35 @@ export default function ServersPage() {
|
||||
</Button>,
|
||||
],
|
||||
}}
|
||||
onSort={async (source, target, items) => {
|
||||
const sourceIndex = items.findIndex((item) => String(item.id) === source);
|
||||
const targetIndex = items.findIndex((item) => String(item.id) === target);
|
||||
|
||||
const originalSorts = items.map((item) => item.sort);
|
||||
|
||||
const [movedItem] = items.splice(sourceIndex, 1);
|
||||
items.splice(targetIndex, 0, movedItem!);
|
||||
|
||||
const updatedItems = items.map((item, index) => {
|
||||
const originalSort = originalSorts[index];
|
||||
const newSort = originalSort !== undefined ? originalSort : item.sort;
|
||||
return { ...item, sort: newSort };
|
||||
});
|
||||
|
||||
const changedItems = updatedItems.filter((item, index) => {
|
||||
return item.sort !== items[index]?.sort;
|
||||
});
|
||||
|
||||
if (changedItems.length > 0) {
|
||||
resetSortWithServer({
|
||||
sort: changedItems.map((item) => ({
|
||||
id: item.id,
|
||||
sort: item.sort,
|
||||
})) as API.SortItem[],
|
||||
});
|
||||
}
|
||||
return updatedItems;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
'use client';
|
||||
|
||||
import { getLogSetting, updateLogSetting } from '@/services/admin/log';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
const logCleanupSchema = z.object({
|
||||
auto_clear: z.boolean(),
|
||||
clear_days: z.number().min(1),
|
||||
});
|
||||
|
||||
type LogCleanupFormData = z.infer<typeof logCleanupSchema>;
|
||||
|
||||
export default function LogCleanupForm() {
|
||||
const t = useTranslations('system');
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getLogSetting'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getLogSetting();
|
||||
return data.data;
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const form = useForm<LogCleanupFormData>({
|
||||
resolver: zodResolver(logCleanupSchema),
|
||||
defaultValues: {
|
||||
auto_clear: false,
|
||||
clear_days: 30,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset(data);
|
||||
}
|
||||
}, [data, form]);
|
||||
|
||||
async function onSubmit(values: LogCleanupFormData) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateLogSetting(values as API.LogSetting);
|
||||
toast.success(t('common.saveSuccess'));
|
||||
refetch();
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast.error(t('common.saveFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<div className='flex cursor-pointer items-center justify-between transition-colors'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
|
||||
<Icon icon='mdi:delete-sweep' className='text-primary h-5 w-5' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium'>{t('logCleanup.title')}</p>
|
||||
<p className='text-muted-foreground text-sm'>{t('logCleanup.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon icon='mdi:chevron-right' className='size-6' />
|
||||
</div>
|
||||
</SheetTrigger>
|
||||
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('logCleanup.title')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id='log-cleanup-form'
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='space-y-2 pt-4'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='auto_clear'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('logCleanup.autoClear')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className='float-end !mt-0'
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('logCleanup.autoClearDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='clear_days'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('logCleanup.clearDays')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
placeholder={t('logCleanup.clearDaysPlaceholder')}
|
||||
value={field.value?.toString()}
|
||||
onValueChange={(value) => field.onChange(Number(value))}
|
||||
disabled={!form.watch('auto_clear')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('logCleanup.clearDaysDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</ScrollArea>
|
||||
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button disabled={loading} type='submit' form='log-cleanup-form'>
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import CurrencyForm from './basic-settings/currency-form';
|
||||
import PrivacyPolicyForm from './basic-settings/privacy-policy-form';
|
||||
import SiteForm from './basic-settings/site-form';
|
||||
import TosForm from './basic-settings/tos-form';
|
||||
import LogCleanupForm from './log-cleanup/log-cleanup-form';
|
||||
import InviteForm from './user-security/invite-form';
|
||||
import RegisterForm from './user-security/register-form';
|
||||
import VerifyCodeForm from './user-security/verify-code-form';
|
||||
@@ -14,7 +15,6 @@ import VerifyForm from './user-security/verify-form';
|
||||
export default function Page() {
|
||||
const t = useTranslations('system');
|
||||
|
||||
// 定义表单配置
|
||||
const formSections = [
|
||||
{
|
||||
title: t('basicSettings'),
|
||||
@@ -34,6 +34,10 @@ export default function Page() {
|
||||
{ component: VerifyCodeForm },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t('logSettings'),
|
||||
forms: [{ component: LogCleanupForm }],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -58,6 +58,8 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
password: z.string().optional(),
|
||||
referer_id: z.number().optional(),
|
||||
refer_code: z.string().optional(),
|
||||
referral_percentage: z.number().optional(),
|
||||
only_first_purchase: z.boolean().optional(),
|
||||
is_admin: z.boolean().optional(),
|
||||
balance: z.number().optional(),
|
||||
gift_amount: z.number().optional(),
|
||||
@@ -218,6 +220,41 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='referral_percentage'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('referralPercentage')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
max={100}
|
||||
placeholder={t('referralPercentagePlaceholder')}
|
||||
{...field}
|
||||
suffix='%'
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, Number(value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='only_first_purchase'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>{t('onlyFirstPurchase')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='balance'
|
||||
|
||||
@@ -30,6 +30,8 @@ const basicInfoSchema = z.object({
|
||||
gift_amount: z.number().optional(),
|
||||
refer_code: z.string().optional(),
|
||||
referer_id: z.number().optional(),
|
||||
referral_percentage: z.number().optional(),
|
||||
only_first_purchase: z.boolean().optional(),
|
||||
is_admin: z.boolean().optional(),
|
||||
password: z.string().optional(),
|
||||
enable: z.boolean(),
|
||||
@@ -52,6 +54,8 @@ export function BasicInfoForm({ user, refetch }: { user: API.User; refetch: () =
|
||||
gift_amount: user.gift_amount,
|
||||
refer_code: user.refer_code,
|
||||
referer_id: user.referer_id,
|
||||
referral_percentage: user.referral_percentage,
|
||||
only_first_purchase: user.only_first_purchase,
|
||||
is_admin: user.is_admin,
|
||||
enable: user.enable,
|
||||
},
|
||||
@@ -216,6 +220,41 @@ export function BasicInfoForm({ user, refetch }: { user: API.User; refetch: () =
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='referral_percentage'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('referralPercentage')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
max={100}
|
||||
value={field.value}
|
||||
suffix='%'
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='only_first_purchase'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>{t('onlyFirstPurchase')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='avatar'
|
||||
|
||||
Reference in New Issue
Block a user