mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-02-06 11:40:28 -05:00
241 lines
6.9 KiB
TypeScript
241 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { Badge } from '@workspace/ui/components/badge';
|
|
import { Button } from '@workspace/ui/components/button';
|
|
import { Card, CardContent } from '@workspace/ui/components/card';
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from '@workspace/ui/components/form';
|
|
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetFooter,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from '@workspace/ui/components/sheet';
|
|
import { Textarea } from '@workspace/ui/components/textarea';
|
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
|
import { useTranslations } from 'next-intl';
|
|
import Image from 'next/image';
|
|
import { useState } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { z } from 'zod';
|
|
|
|
interface Client {
|
|
name: string;
|
|
platforms: string[];
|
|
}
|
|
|
|
const clientFormSchema = z.object({
|
|
template: z.string().optional(),
|
|
});
|
|
|
|
const clientsConfig = [
|
|
{
|
|
name: 'Hiddify',
|
|
platforms: ['Windows', 'macOS', 'Linux', 'iOS', 'Android'],
|
|
},
|
|
{
|
|
name: 'SingBox',
|
|
platforms: ['Windows', 'macOS', 'Linux', 'iOS', 'Android'],
|
|
},
|
|
{
|
|
name: 'Clash',
|
|
platforms: ['Windows', 'macOS', 'Linux', 'Android'],
|
|
},
|
|
{
|
|
name: 'V2rayN',
|
|
platforms: ['Windows', 'macOS', 'Linux'],
|
|
},
|
|
{
|
|
name: 'Stash',
|
|
platforms: ['macOS', 'iOS'],
|
|
},
|
|
{
|
|
name: 'Surge',
|
|
platforms: ['macOS', 'iOS'],
|
|
},
|
|
{
|
|
name: 'V2Box',
|
|
platforms: ['macOS', 'iOS'],
|
|
},
|
|
{
|
|
name: 'Shadowrocket',
|
|
platforms: ['iOS'],
|
|
},
|
|
{
|
|
name: 'Quantumult',
|
|
platforms: ['iOS'],
|
|
},
|
|
{
|
|
name: 'Loon',
|
|
platforms: ['iOS'],
|
|
},
|
|
{
|
|
name: 'Egern',
|
|
platforms: ['iOS'],
|
|
},
|
|
{
|
|
name: 'V2rayNG',
|
|
platforms: ['Android'],
|
|
},
|
|
{
|
|
name: 'Surfboard',
|
|
platforms: ['Android'],
|
|
},
|
|
{
|
|
name: 'Netch',
|
|
platforms: ['Windows'],
|
|
},
|
|
];
|
|
|
|
type ClientFormData = z.infer<typeof clientFormSchema>;
|
|
|
|
export function ProtocolForm() {
|
|
const t = useTranslations('subscribe');
|
|
const [selectedClient, setSelectedClient] = useState<Client | null>(null);
|
|
const [open, setOpen] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const form = useForm<ClientFormData>({
|
|
resolver: zodResolver(clientFormSchema),
|
|
defaultValues: {
|
|
template: '',
|
|
},
|
|
});
|
|
|
|
const clients: Client[] = clientsConfig;
|
|
|
|
const handleClientClick = (client: Client) => {
|
|
setSelectedClient(client);
|
|
// 请求当前客户端的配置模板
|
|
// 这里可以替换为实际的API调用
|
|
// 模拟获取模板数据
|
|
const mockTemplate = `# ${client.name} 配置模板`;
|
|
form.reset({
|
|
template: mockTemplate,
|
|
});
|
|
setOpen(true);
|
|
};
|
|
|
|
const onSubmit = async (data: ClientFormData) => {
|
|
setLoading(true);
|
|
try {
|
|
// TODO: 实现保存逻辑
|
|
console.log('Save client template:', {
|
|
name: selectedClient?.name,
|
|
template: data.template,
|
|
});
|
|
// 模拟API调用
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
setOpen(false);
|
|
} catch (error) {
|
|
console.error('Failed to save client template:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className='space-y-4'>
|
|
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
|
|
{clients.map((client) => (
|
|
<Card
|
|
key={client.name}
|
|
className='hover:bg-muted/50 cursor-pointer transition-colors'
|
|
onClick={() => handleClientClick(client)}
|
|
>
|
|
<CardContent className='p-4'>
|
|
<div className='space-y-3'>
|
|
<div className='flex items-center gap-2'>
|
|
<div className='relative h-6 w-6 flex-shrink-0'>
|
|
<Image
|
|
src={`/images/protocols/${client.name}.webp`}
|
|
alt={client.name}
|
|
width={24}
|
|
height={24}
|
|
className='object-contain'
|
|
onError={() => {
|
|
console.log(`Failed to load image for ${client.name}`);
|
|
}}
|
|
/>
|
|
</div>
|
|
<h3 className='text-base font-semibold'>{client.name}</h3>
|
|
<Icon icon='mdi:chevron-right' className='ml-auto flex-shrink-0' />
|
|
</div>
|
|
|
|
<div className='flex flex-wrap gap-1'>
|
|
{client.platforms.map((platform) => (
|
|
<Badge key={platform} variant='secondary' className='text-xs'>
|
|
{platform}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
|
|
<p className='text-muted-foreground text-sm leading-relaxed'>
|
|
{t(`protocol.clients.${client.name.toLowerCase().replace(/\s+/g, '')}.features`)}
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
|
|
<SheetHeader>
|
|
<SheetTitle>
|
|
{t('protocol.subscribeTemplate')} - {selectedClient?.name}
|
|
</SheetTitle>
|
|
</SheetHeader>
|
|
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
|
|
<Form {...form}>
|
|
<form
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
className='space-y-4 pt-4'
|
|
id='client-template-form'
|
|
>
|
|
<FormField
|
|
control={form.control}
|
|
name='template'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('protocol.templateContent')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder={t('protocol.templatePlaceholder')}
|
|
value={field.value}
|
|
onChange={field.onChange}
|
|
rows={20}
|
|
className='font-mono text-sm'
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</form>
|
|
</Form>
|
|
</ScrollArea>
|
|
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
|
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
|
|
{t('actions.cancel')}
|
|
</Button>
|
|
<Button disabled={loading} type='submit' form='client-template-form'>
|
|
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
|
{t('actions.saveTemplate')}
|
|
</Button>
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
</div>
|
|
);
|
|
}
|