🎉 chore(init): project initialization

This commit is contained in:
web@ppanel
2024-11-14 01:22:43 +07:00
commit 829edfa824
479 changed files with 61413 additions and 0 deletions
@@ -0,0 +1,137 @@
'use client';
import { Icon } from '@iconify/react';
import { EnhancedInput } from '@repo/ui/enhanced-input';
import { Button } from '@shadcn/ui/button';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@shadcn/ui/form';
import { useForm } from '@shadcn/ui/lib/react-hook-form';
import { z, zodResolver } from '@shadcn/ui/lib/zod';
import { ScrollArea } from '@shadcn/ui/scroll-area';
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@shadcn/ui/sheet';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
const formSchema = z.object({
name: z.string(),
description: z.string().optional(),
});
interface GroupFormProps<T> {
onSubmit: (data: T) => Promise<boolean> | boolean;
initialValues?: T;
loading?: boolean;
trigger: string;
title: string;
}
export default function GroupForm<T extends Record<string, any>>({
onSubmit,
initialValues,
loading,
trigger,
title,
}: GroupFormProps<T>) {
const t = useTranslations('server');
const [open, setOpen] = useState(false);
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
...initialValues,
},
});
useEffect(() => {
form?.reset(initialValues);
}, [form, initialValues]);
async function handleSubmit(data: { [x: string]: any }) {
const bool = await onSubmit(data as T);
if (bool) setOpen(false);
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
onClick={() => {
form.reset();
setOpen(true);
}}
>
{trigger}
</Button>
</SheetTrigger>
<SheetContent className='w-[500px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-4 px-6 pt-4'>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('group.form.name')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='description'
render={({ field }) => (
<FormItem>
<FormLabel>{t('group.form.description')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</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('group.form.cancel')}
</Button>
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}{' '}
{t('group.form.confirm')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,140 @@
'use client';
import { ProTable, ProTableActions } from '@/components/pro-table';
import {
batchDeleteNodeGroup,
createNodeGroup,
deleteNodeGroup,
getNodeGroupList,
updateNodeGroup,
} from '@/services/admin/server';
import { ConfirmButton } from '@repo/ui/confirm-button';
import { formatDate } from '@repo/ui/utils';
import { Button } from '@shadcn/ui/button';
import { toast } from '@shadcn/ui/lib/sonner';
import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react';
import GroupForm from './group-form';
export default function GroupTable() {
const t = useTranslations('server');
const [loading, setLoading] = useState(false);
const ref = useRef<ProTableActions>();
return (
<ProTable<API.ServerGroup, any>
action={ref}
header={{
title: t('group.title'),
toolbar: (
<GroupForm<API.CreateNodeGroupRequest>
trigger={t('group.create')}
title={t('group.createNodeGroup')}
loading={loading}
onSubmit={async (values) => {
setLoading(true);
try {
await createNodeGroup(values);
toast.success(t('group.createdSuccessfully'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>
),
}}
columns={[
{
accessorKey: 'name',
header: t('group.name'),
},
{
accessorKey: 'description',
header: t('group.description'),
cell: ({ row }) => <p className='line-clamp-3'>{row.getValue('description')}</p>,
},
{
accessorKey: 'updated_at',
header: t('group.updatedAt'),
cell: ({ row }) => formatDate(row.getValue('updated_at')),
},
]}
request={async () => {
const { data } = await getNodeGroupList();
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
actions={{
render: (row) => [
<GroupForm<API.ServerGroup>
key='edit'
trigger={t('group.edit')}
title={t('group.editNodeGroup')}
loading={loading}
initialValues={row}
onSubmit={async (values) => {
setLoading(true);
try {
await updateNodeGroup({
...row,
...values,
});
toast.success(t('group.createdSuccessfully'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>,
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('group.delete')}</Button>}
title={t('group.confirmDelete')}
description={t('group.deleteWarning')}
onConfirm={async () => {
await deleteNodeGroup({
id: row.id!,
});
toast.success(t('group.deletedSuccessfully'));
ref.current?.refresh();
}}
cancelText={t('group.cancel')}
confirmText={t('group.confirm')}
/>,
],
batchRender(rows) {
return [
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('group.delete')}</Button>}
title={t('group.confirmDelete')}
description={t('group.deleteWarning')}
onConfirm={async () => {
await batchDeleteNodeGroup({
ids: rows.map((item) => item.id),
});
toast.success(t('group.deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('group.cancel')}
confirmText={t('group.confirm')}
/>,
];
},
}}
/>
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,225 @@
'use client';
import { Display } from '@/components/display';
import { ProTable, ProTableActions } from '@/components/pro-table';
import {
batchDeleteNode,
createNode,
deleteNode,
getNodeGroupList,
getNodeList,
updateNode,
} from '@/services/admin/server';
import { ConfirmButton } from '@repo/ui/confirm-button';
import { Badge } from '@shadcn/ui/badge';
import { Button } from '@shadcn/ui/button';
import { toast } from '@shadcn/ui/lib/sonner';
import { cn } from '@shadcn/ui/lib/utils';
import { Switch } from '@shadcn/ui/switch';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@shadcn/ui/tooltip';
import { useQuery } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react';
import NodeForm from './node-form';
export default function NodeTable() {
const t = useTranslations('server.node');
const [loading, setLoading] = useState(false);
const { data: groups } = useQuery({
queryKey: ['getNodeGroupList'],
queryFn: async () => {
const { data } = await getNodeGroupList();
return (data.data?.list || []) as API.ServerGroup[];
},
});
const ref = useRef<ProTableActions>();
return (
<ProTable<API.Server, { groupId: number; search: string }>
action={ref}
header={{
toolbar: (
<NodeForm<API.CreateNodeRequest>
trigger={t('create')}
title={t('createNode')}
loading={loading}
onSubmit={async (values) => {
setLoading(true);
try {
await createNode({ ...values, enable: false });
toast.success(t('createSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>
),
}}
columns={[
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Badge
variant='outline'
className={cn('text-primary-foreground', {
'bg-green-500': row.original.protocol === 'shadowsocks',
'bg-rose-500': row.original.protocol === 'vmess',
'bg-blue-500': row.original.protocol === 'vless',
'bg-yellow-500': row.original.protocol === 'trojan',
})}
>
{row.getValue('id')}
</Badge>
</TooltipTrigger>
<TooltipContent>{row.original.protocol}</TooltipContent>
</Tooltip>
</TooltipProvider>
),
},
{
accessorKey: 'enable',
header: t('enable'),
cell: ({ row }) => {
return (
<Switch
checked={row.getValue('enable')}
onCheckedChange={async (checked) => {
await updateNode({
...row.original,
id: row.original.id!,
enable: checked,
} as API.UpdateNodeRequest);
ref.current?.refresh();
}}
/>
);
},
},
{
accessorKey: 'name',
header: t('name'),
},
{
accessorKey: 'server_addr',
header: t('serverAddr'),
},
{
accessorKey: 'speed_limit',
header: t('speedLimit'),
cell: ({ row }) => (
<Display type='traffic' value={row.getValue('speed_limit')} unlimited />
),
},
{
accessorKey: 'traffic_ratio',
header: t('trafficRatio'),
cell: ({ row }) => <Badge variant='outline'>{row.getValue('traffic_ratio')} X</Badge>,
},
{
accessorKey: 'groupId',
header: t('nodeGroup'),
cell: ({ row }) => {
const name = groups?.find((group) => group.id === row.getValue('groupId'))?.name;
return name ? <Badge variant='outline'>{name}</Badge> : '--';
},
},
]}
params={[
{
key: 'search',
},
{
key: 'group_id',
placeholder: t('nodeGroup'),
options: groups?.map((item) => ({
label: item.name,
value: String(item.id),
})),
},
]}
request={async (pagination, filter) => {
const { data } = await getNodeList({
...pagination,
...filter,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
actions={{
render: (row) => [
<NodeForm<API.Server>
key='edit'
trigger={t('edit')}
title={t('editNode')}
loading={loading}
initialValues={row}
onSubmit={async (values) => {
setLoading(true);
try {
await updateNode({ ...row, ...values } as API.UpdateNodeRequest);
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 deleteNode({
id: row.id,
});
toast.success(t('deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
],
batchRender(rows) {
return [
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('delete')}</Button>}
title={t('group.confirmDelete')}
description={t('group.deleteWarning')}
onConfirm={async () => {
await batchDeleteNode({
ids: rows.map((item) => item.id),
});
toast.success(t('group.deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('group.cancel')}
confirmText={t('group.confirm')}
/>,
];
},
}}
/>
);
}
+24
View File
@@ -0,0 +1,24 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@shadcn/ui/tabs';
import { getTranslations } from 'next-intl/server';
import GroupTable from './group-table';
import NodeTable from './node-table';
export default async function Page() {
const t = await getTranslations('server');
return (
<Tabs defaultValue='node'>
<TabsList>
<TabsTrigger value='node'>{t('tabs.node')}</TabsTrigger>
<TabsTrigger value='group'>{t('tabs.nodeGroup')}</TabsTrigger>
</TabsList>
<TabsContent value='node'>
<NodeTable />
</TabsContent>
<TabsContent value='group'>
<GroupTable />
</TabsContent>
</Tabs>
);
}