✨ feat(user): Add User Detail
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user