feat(user): Add User Detail

This commit is contained in:
web@ppanel
2025-02-02 14:40:29 +07:00
parent 6733fc211c
commit fdaf11b654
66 changed files with 1977 additions and 704 deletions
@@ -0,0 +1,143 @@
'use client';
import { Display } from '@/components/display';
import { ProTable, ProTableActions } from '@/components/pro-table';
import { Button } from '@workspace/ui/components/button';
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
import { formatDate } from '@workspace/ui/utils';
import { useRef, useState } from 'react';
import { SubscriptionDetail } from './subscription-detail';
import { SubscriptionForm } from './subscription-form';
// 模拟数据
const mockData: API.Subscribe[] = [
{
id: 1,
name: 'Basic Package',
description: 'Basic Traffic Package',
unit_price: 9.9,
unit_time: '30d',
discount: [],
replacement: 0,
inventory: 100,
traffic: 1073741824, // 1GB
speed_limit: 10,
device_limit: 3,
quota: 0,
group_id: 1,
server_group: [1],
server: [1, 2],
show: true,
sell: true,
sort: 1,
deduction_ratio: 0,
allow_deduction: false,
reset_cycle: 30,
renewal_reset: true,
created_at: Date.now(),
updated_at: Date.now(),
},
// 可以添加更多模拟数据...
];
interface Props {
userId: string;
}
export default function UserSubscription({ userId }: Props) {
const [loading, setLoading] = useState(false);
const ref = useRef<ProTableActions>(null);
return (
<ProTable<API.Subscribe, Record<string, unknown>>
action={ref}
header={{
title: 'Subscription List',
toolbar: (
<SubscriptionForm
key='create'
trigger={<Button>Create</Button>}
title='Create Subscription'
loading={loading}
userId={userId}
onSubmit={async (values) => {
console.log('创建订阅:', values);
return true;
}}
/>
),
}}
columns={[
{
accessorKey: 'id',
header: 'ID',
},
{
accessorKey: 'name',
header: '名称',
},
{
accessorKey: 'traffic',
header: '流量',
cell: ({ row }) => <Display type='traffic' value={row.getValue('traffic')} />,
},
{
accessorKey: 'speed_limit',
header: '限速',
cell: ({ row }) => `${row.getValue('speed_limit')} Mbps`,
},
{
accessorKey: 'device_limit',
header: '设备限制',
},
{
accessorKey: 'created_at',
header: '创建时间',
cell: ({ row }) => formatDate(row.getValue('created_at')),
},
]}
request={async () => {
// 模拟异步请求
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
list: mockData,
total: mockData.length,
};
}}
actions={{
render: (row) => {
return [
<SubscriptionForm
key='edit'
trigger={<Button>Edit</Button>}
title='Edit Subscription'
loading={loading}
userId={userId}
initialData={row}
onSubmit={async (values) => {
console.log('编辑订阅:', values);
return true;
}}
/>,
<SubscriptionDetail
key='detail'
trigger={<Button variant='secondary'>Details</Button>}
subscriptionId={row.id.toString()}
/>,
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>Delete</Button>}
title='Confirm Delete'
description='Are you sure to delete this subscription?'
onConfirm={async () => {
console.log('删除订阅:', row.id);
}}
cancelText='Cancel'
confirmText='Confirm'
/>,
];
},
}}
/>
);
}
@@ -0,0 +1,152 @@
'use client';
import { Display } from '@/components/display';
import { ProTable } from '@/components/pro-table';
import { Button } from '@workspace/ui/components/button';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@workspace/ui/components/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
import { formatDate } from '@workspace/ui/utils';
import { ReactNode, useState } from 'react';
import { toast } from 'sonner';
// 模拟数据
const mockLogs = [
{ id: 1, action: 'Create Subscription', created_at: Date.now() - 86400000 },
{ id: 2, action: 'Update Traffic', created_at: Date.now() - 3600000 },
];
const mockTrafficLogs = [
{ id: 1, traffic: 104857600, created_at: Date.now() - 86400000 },
{ id: 2, traffic: 52428800, created_at: Date.now() - 3600000 },
];
const mockDevices = [
{ id: 1, ip: '192.168.1.1', last_seen_at: Date.now() - 300000 },
{ id: 2, ip: '192.168.1.2', last_seen_at: Date.now() - 600000 },
];
interface Props {
trigger: ReactNode;
subscriptionId: string;
}
export function SubscriptionDetail({ trigger }: Props) {
const [open, setOpen] = useState(false);
// 模拟下线设备的函数
const handleOfflineDevice = async (deviceId: number) => {
// TODO: 调用实际的API
console.log('下线设备:', deviceId);
toast.success('设备已下线');
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent className='max-w-5xl'>
<DialogHeader>
<DialogTitle>Subscription Details</DialogTitle>
</DialogHeader>
<div className='mt-4'>
<Tabs defaultValue='logs'>
<TabsList className='w-full'>
<TabsTrigger value='logs' className='flex-1'>
Subscription Logs
</TabsTrigger>
<TabsTrigger value='traffic' className='flex-1'>
Traffic Logs
</TabsTrigger>
<TabsTrigger value='devices' className='flex-1'>
Online Devices
</TabsTrigger>
</TabsList>
<TabsContent value='logs'>
<ProTable
columns={[
{
accessorKey: 'action',
header: 'Action',
},
{
accessorKey: 'created_at',
header: 'Time',
cell: ({ row }) => formatDate(row.getValue('created_at')),
},
]}
request={async () => ({
list: mockLogs,
total: mockLogs.length,
})}
/>
</TabsContent>
<TabsContent value='traffic'>
<ProTable
columns={[
{
accessorKey: 'traffic',
header: 'Traffic',
cell: ({ row }) => <Display type='traffic' value={row.getValue('traffic')} />,
},
{
accessorKey: 'created_at',
header: 'Time',
cell: ({ row }) => formatDate(row.getValue('created_at')),
},
]}
request={async () => ({
list: mockTrafficLogs,
total: mockTrafficLogs.length,
})}
/>
</TabsContent>
<TabsContent value='devices'>
<ProTable
columns={[
{
accessorKey: 'ip',
header: 'IP',
},
{
accessorKey: 'last_seen_at',
header: 'Last Seen',
cell: ({ row }) => formatDate(row.getValue('last_seen_at')),
},
]}
request={async () => ({
list: mockDevices,
total: mockDevices.length,
})}
actions={{
render: (row) => {
return [
<ConfirmButton
key='offline'
trigger={
<Button variant='destructive' size='sm'>
线
</Button>
}
title='Confirm Offline'
description={`Are you sure to offline IP ${row.ip}?`}
onConfirm={() => handleOfflineDevice(row.id)}
cancelText='Cancel'
confirmText='Confirm'
/>,
];
},
}}
/>
</TabsContent>
</Tabs>
</div>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,127 @@
'use client';
import { Button } from '@workspace/ui/components/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@workspace/ui/components/form';
import { Input } from '@workspace/ui/components/input';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { ReactNode, useState } from 'react';
import { useForm } from 'react-hook-form';
interface Props {
trigger: ReactNode;
title: string;
loading?: boolean;
userId: string;
initialData?: API.Subscribe;
onSubmit: (values: any) => Promise<boolean>;
}
export function SubscriptionForm({
trigger,
title,
loading,
userId,
initialData,
onSubmit,
}: Props) {
const [open, setOpen] = useState(false);
const form = useForm({
defaultValues: {
user_id: userId,
name: initialData?.name || '',
traffic: initialData?.traffic || 0,
speed_limit: initialData?.speed_limit || 0,
device_limit: initialData?.device_limit || 0,
...(initialData && { id: initialData.id }),
},
});
const handleSubmit = async (values: any) => {
const success = await onSubmit(values);
if (success) {
setOpen(false);
form.reset();
}
};
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>{trigger}</SheetTrigger>
<SheetContent side='right'>
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
</SheetHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className='mt-4 space-y-4'>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='traffic'
render={({ field }) => (
<FormItem>
<FormLabel>Traffic (Bytes)</FormLabel>
<FormControl>
<Input type='number' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='speed_limit'
render={({ field }) => (
<FormItem>
<FormLabel>Speed Limit (Mbps)</FormLabel>
<FormControl>
<Input type='number' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='device_limit'
render={({ field }) => (
<FormItem>
<FormLabel>Device Limit</FormLabel>
<FormControl>
<Input type='number' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type='submit'>Submit</Button>
</form>
</Form>
</SheetContent>
</Sheet>
);
}