feat(dashboard): Optimization

This commit is contained in:
web@ppanel 2024-11-27 17:35:54 +07:00
parent 2992824353
commit 5b3f4b493f

View File

@ -1,8 +1,15 @@
'use client'; 'use client';
import { formatBytes } from '@repo/ui/utils'; import { Icon } from '@iconify/react';
import { Badge } from '@shadcn/ui/badge'; import { formatBytes, unitConversion } from '@repo/ui/utils';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@shadcn/ui/card'; import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@shadcn/ui/card';
import { import {
ChartConfig, ChartConfig,
ChartContainer, ChartContainer,
@ -18,13 +25,17 @@ import {
BarChart, BarChart,
CartesianGrid, CartesianGrid,
Label, Label,
LabelList,
Pie, Pie,
PieChart, PieChart,
XAxis, XAxis,
YAxis,
} from '@shadcn/ui/lib/recharts'; } from '@shadcn/ui/lib/recharts';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@shadcn/ui/select';
import { Separator } from '@shadcn/ui/separator'; import { Separator } from '@shadcn/ui/separator';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@shadcn/ui/table'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@shadcn/ui/tabs';
import { useLocale } from 'next-intl'; import { useLocale } from 'next-intl';
import { useState } from 'react';
const UserStatisticsConfig = { const UserStatisticsConfig = {
register: { register: {
@ -52,68 +63,140 @@ const IncomeStatisticsConfig = {
}, },
}; };
// Sample data - replace with actual data
const trafficData = {
nodes: {
today: [
{ name: 'Node 1', traffic: 1000, type: 'Trojan', address: '127.0.0.1:443' },
{ name: 'Node 2', traffic: 800, type: 'Trojan', address: '127.0.0.1:444' },
{ name: 'Node 3', traffic: 600, type: 'Trojan', address: '127.0.0.1:445' },
{ name: 'Node 4', traffic: 400, type: 'Trojan', address: '127.0.0.1:446' },
{ name: 'Node 5', traffic: 200, type: 'Trojan', address: '127.0.0.1:447' },
{ name: 'Node 6', traffic: 1000, type: 'Trojan', address: '127.0.0.1:443' },
{ name: 'Node 7', traffic: 800, type: 'Trojan', address: '127.0.0.1:444' },
{ name: 'Node 8', traffic: 600, type: 'Trojan', address: '127.0.0.1:445' },
{ name: 'Node 9', traffic: 400, type: 'Trojan', address: '127.0.0.1:446' },
{ name: 'Node 10', traffic: 200, type: 'Trojan', address: '127.0.0.1:447' },
],
yesterday: [
{ name: 'Node 1', traffic: 900, type: 'Trojan', address: '127.0.0.1:443' },
{ name: 'Node 2', traffic: 750, type: 'Trojan', address: '127.0.0.1:444' },
{ name: 'Node 3', traffic: 550, type: 'Trojan', address: '127.0.0.1:445' },
{ name: 'Node 4', traffic: 350, type: 'Trojan', address: '127.0.0.1:446' },
{ name: 'Node 5', traffic: 150, type: 'Trojan', address: '127.0.0.1:447' },
],
},
users: {
today: [
{ name: 'olivia.martin@email.com', traffic: 100, email: 'olivia.martin@email.com' },
{ name: 'jackson.lee@email.com', traffic: 90, email: 'jackson.lee@email.com' },
{ name: 'isabella.nguyen@email.com', traffic: 80, email: 'isabella.nguyen@email.com' },
{ name: 'william.chen@email.com', traffic: 70, email: 'william.chen@email.com' },
{ name: 'sophia.rodriguez@email.com', traffic: 60, email: 'sophia.rodriguez@email.com' },
{ name: 'olivia.martin@email.com', traffic: 100, email: 'olivia.martin@email.com' },
{ name: 'jackson.lee@email.com', traffic: 90, email: 'jackson.lee@email.com' },
{ name: 'isabella.nguyen@email.com', traffic: 80, email: 'isabella.nguyen@email.com' },
{ name: 'william.chen@email.com', traffic: 70, email: 'william.chen@email.com' },
{ name: 'sophia.rodriguez@email.com', traffic: 60, email: 'sophia.rodriguez@email.com' },
],
yesterday: [
{ name: 'olivia.martin@email.com', traffic: 95, email: 'olivia.martin@email.com' },
{ name: 'jackson.lee@email.com', traffic: 85, email: 'jackson.lee@email.com' },
{ name: 'isabella.nguyen@email.com', traffic: 75, email: 'isabella.nguyen@email.com' },
{ name: 'william.chen@email.com', traffic: 65, email: 'william.chen@email.com' },
{ name: 'sophia.rodriguez@email.com', traffic: 55, email: 'sophia.rodriguez@email.com' },
],
},
};
export default function Dashboard() { export default function Dashboard() {
const locale = useLocale(); const locale = useLocale();
return (
<div className='flex flex-1 flex-col gap-4'>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className='grid grid-cols-2 gap-4 lg:grid-cols-4'>
<div className='grid flex-1 auto-rows-min gap-0.5'>
<div className='text-muted-foreground text-sm'>线IP数</div>
<div className='text-xl font-bold tabular-nums leading-none'>666</div>
</div>
<div className='grid flex-1 auto-rows-min gap-0.5'>
<div className='text-muted-foreground text-sm'>线</div>
<div className='text-xl font-bold tabular-nums leading-none'>99</div>
</div>
<div className='grid flex-1 auto-rows-min gap-0.5'>
<div className='text-muted-foreground text-sm'>线</div>
<div className='text-xl font-bold tabular-nums leading-none'>1</div>
</div>
<div className='grid flex-1 auto-rows-min gap-0.5'> const [dataType, setDataType] = useState<string | 'nodes' | 'users'>('nodes');
<div className='text-muted-foreground text-sm'></div> const [timeFrame, setTimeFrame] = useState<string | 'today' | 'yesterday'>('today');
<div className='text-xl font-bold tabular-nums leading-none'>1</div>
</div> const currentData = trafficData[dataType][timeFrame];
<div className='grid flex-1 auto-rows-min gap-0.5'>
<div className='text-muted-foreground text-sm'></div> return (
<div className='text-xl font-bold tabular-nums leading-none'> <div className='flex flex-1 flex-col gap-3'>
{formatBytes(99999999999999)} <div className='grid grid-cols-2 gap-3 md:grid-cols-4 lg:grid-cols-8'>
</div> {[
</div> {
<div className='grid flex-1 auto-rows-min gap-0.5'> title: '在线IP数',
<div className='text-muted-foreground text-sm'></div> value: '666',
<div className='text-xl font-bold tabular-nums leading-none'> icon: 'uil:network-wired',
{formatBytes(99999999999999)} onClick: () => console.log('在线IP数 clicked'),
</div> },
</div> {
<div className='grid flex-1 auto-rows-min gap-0.5'> title: '在线节点数',
<div className='text-muted-foreground text-sm'></div> value: '99',
<div className='text-xl font-bold tabular-nums leading-none'> icon: 'uil:server-network',
{formatBytes(99999999999999)} onClick: () => console.log('在线节点数 clicked'),
</div> },
</div> {
<div className='grid flex-1 auto-rows-min gap-0.5'> title: '离线节点数',
<div className='text-muted-foreground text-sm'></div> value: '1',
<div className='text-xl font-bold tabular-nums leading-none'> icon: 'uil:server-network-alt',
{formatBytes(99999999999999)} onClick: () => console.log('离线节点数 clicked'),
</div> },
</div> {
</CardContent> title: '待处理工单',
</Card> value: '1',
<div className='grid gap-4 lg:grid-cols-3'> icon: 'uil:clipboard-notes',
<Card> onClick: () => console.log('待处理工单 clicked'),
<CardHeader className='items-center'> },
<CardTitle></CardTitle> {
title: '今日上传流量',
value: formatBytes(99999999999999),
icon: 'uil:arrow-up',
onClick: () => console.log('今日上传流量 clicked'),
},
{
title: '今日下载流量',
value: formatBytes(99999999999999),
icon: 'uil:arrow-down',
onClick: () => console.log('今日下载流量 clicked'),
},
{
title: '总上传流量',
value: formatBytes(99999999999999),
icon: 'uil:cloud-upload',
onClick: () => console.log('总上传流量 clicked'),
},
{
title: '总下载流量',
value: formatBytes(99999999999999),
icon: 'uil:cloud-download',
onClick: () => console.log('总下载流量 clicked'),
},
].map((item, index) => (
<Card key={index} onClick={item.onClick} className='cursor-pointer'>
<CardHeader className='flex flex-row items-center justify-between'>
<CardTitle>{item.title}</CardTitle>
<CardDescription>
<Icon icon={item.icon} className='text-2xl' />
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<ChartContainer <div className='text-xl font-bold tabular-nums leading-none'>{item.value}</div>
config={IncomeStatisticsConfig} </CardContent>
className='mx-auto aspect-square max-h-[213px]' </Card>
> ))}
</div>
<div className='grid gap-3 md:grid-cols-2 lg:grid-cols-3'>
<Tabs defaultValue='today'>
<Card>
<CardHeader className='flex !flex-row items-center justify-between'>
<CardTitle></CardTitle>
<TabsList>
<TabsTrigger value='today'></TabsTrigger>
<TabsTrigger value='month'></TabsTrigger>
<TabsTrigger value='total'></TabsTrigger>
</TabsList>
</CardHeader>
<TabsContent value='today'>
<CardContent>
<ChartContainer config={IncomeStatisticsConfig} className='mx-auto max-h-80'>
<PieChart> <PieChart>
<ChartLegend content={<ChartLegendContent />} /> <ChartLegend content={<ChartLegendContent />} />
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} /> <ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
@ -161,7 +244,7 @@ export default function Dashboard() {
</PieChart> </PieChart>
</ChartContainer> </ChartContainer>
</CardContent> </CardContent>
<CardFooter className='flex flex-row border-t p-4'> <CardFooter className='flex h-20 flex-row border-t p-4'>
<div className='flex w-full items-center gap-2'> <div className='flex w-full items-center gap-2'>
<div className='grid flex-1 auto-rows-min gap-0.5'> <div className='grid flex-1 auto-rows-min gap-0.5'>
<div className='text-muted-foreground text-xs'></div> <div className='text-muted-foreground text-xs'></div>
@ -183,14 +266,10 @@ export default function Dashboard() {
</div> </div>
</div> </div>
</CardFooter> </CardFooter>
</Card> </TabsContent>
<TabsContent value='month'>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent> <CardContent>
<ChartContainer config={IncomeStatisticsConfig}> <ChartContainer config={IncomeStatisticsConfig} className='max-h-80 w-full'>
<BarChart <BarChart
accessibilityLayer accessibilityLayer
data={[ data={[
@ -222,7 +301,7 @@ export default function Dashboard() {
</BarChart> </BarChart>
</ChartContainer> </ChartContainer>
</CardContent> </CardContent>
<CardFooter className='flex flex-row border-t p-4'> <CardFooter className='flex h-20 flex-row border-t p-4'>
<div className='flex w-full items-center gap-2'> <div className='flex w-full items-center gap-2'>
<div className='grid flex-1 auto-rows-min gap-0.5'> <div className='grid flex-1 auto-rows-min gap-0.5'>
<div className='text-muted-foreground text-xs'></div> <div className='text-muted-foreground text-xs'></div>
@ -244,14 +323,10 @@ export default function Dashboard() {
</div> </div>
</div> </div>
</CardFooter> </CardFooter>
</Card> </TabsContent>
<TabsContent value='total'>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent> <CardContent>
<ChartContainer config={IncomeStatisticsConfig}> <ChartContainer config={IncomeStatisticsConfig} className='max-h-80 w-full'>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={[ data={[
@ -279,7 +354,10 @@ export default function Dashboard() {
}); });
}} }}
/> />
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator='dot' />} /> <ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator='dot' />}
/>
<Area <Area
dataKey='new_purchase' dataKey='new_purchase'
type='natural' type='natural'
@ -300,7 +378,7 @@ export default function Dashboard() {
</AreaChart> </AreaChart>
</ChartContainer> </ChartContainer>
</CardContent> </CardContent>
<CardFooter className='flex flex-row border-t p-4'> <CardFooter className='flex h-20 flex-row border-t p-4'>
<div className='flex w-full items-center gap-2'> <div className='flex w-full items-center gap-2'>
<div className='grid flex-1 auto-rows-min gap-0.5'> <div className='grid flex-1 auto-rows-min gap-0.5'>
<div className='text-muted-foreground text-xs'></div> <div className='text-muted-foreground text-xs'></div>
@ -308,17 +386,22 @@ export default function Dashboard() {
</div> </div>
</div> </div>
</CardFooter> </CardFooter>
</TabsContent>
</Card> </Card>
</Tabs>
<Tabs defaultValue='today'>
<Card> <Card>
<CardHeader className='items-center'> <CardHeader className='flex !flex-row items-center justify-between'>
<CardTitle></CardTitle> <CardTitle></CardTitle>
<TabsList>
<TabsTrigger value='today'></TabsTrigger>
<TabsTrigger value='month'></TabsTrigger>
<TabsTrigger value='total'></TabsTrigger>
</TabsList>
</CardHeader> </CardHeader>
<TabsContent value='today'>
<CardContent> <CardContent>
<ChartContainer <ChartContainer config={UserStatisticsConfig} className='mx-auto max-h-80'>
config={UserStatisticsConfig}
className='mx-auto aspect-square max-h-[213px]'
>
<PieChart> <PieChart>
<ChartLegend content={<ChartLegendContent />} /> <ChartLegend content={<ChartLegendContent />} />
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} /> <ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
@ -371,7 +454,7 @@ export default function Dashboard() {
</PieChart> </PieChart>
</ChartContainer> </ChartContainer>
</CardContent> </CardContent>
<CardFooter className='flex flex-row border-t p-4'> <CardFooter className='flex h-20 flex-row border-t p-4'>
<div className='flex w-full items-center gap-2'> <div className='flex w-full items-center gap-2'>
<div className='grid flex-1 auto-rows-min gap-0.5'> <div className='grid flex-1 auto-rows-min gap-0.5'>
<div className='text-muted-foreground text-xs'> <div className='text-muted-foreground text-xs'>
@ -395,14 +478,10 @@ export default function Dashboard() {
</div> </div>
</div> </div>
</CardFooter> </CardFooter>
</Card> </TabsContent>
<TabsContent value='month'>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent> <CardContent>
<ChartContainer config={UserStatisticsConfig}> <ChartContainer config={UserStatisticsConfig} className='max-h-80 w-full'>
<BarChart <BarChart
accessibilityLayer accessibilityLayer
data={[ data={[
@ -435,7 +514,7 @@ export default function Dashboard() {
</BarChart> </BarChart>
</ChartContainer> </ChartContainer>
</CardContent> </CardContent>
<CardFooter className='flex flex-row border-t p-4'> <CardFooter className='flex h-20 flex-row border-t p-4'>
<div className='flex w-full items-center gap-2'> <div className='flex w-full items-center gap-2'>
<div className='grid flex-1 auto-rows-min gap-0.5'> <div className='grid flex-1 auto-rows-min gap-0.5'>
<div className='text-muted-foreground text-xs'> <div className='text-muted-foreground text-xs'>
@ -459,14 +538,10 @@ export default function Dashboard() {
</div> </div>
</div> </div>
</CardFooter> </CardFooter>
</Card> </TabsContent>
<TabsContent value='total'>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent> <CardContent>
<ChartContainer config={UserStatisticsConfig}> <ChartContainer config={UserStatisticsConfig} className='max-h-80 w-full'>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={[ data={[
@ -494,7 +569,10 @@ export default function Dashboard() {
}); });
}} }}
/> />
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator='dot' />} /> <ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator='dot' />}
/>
<Area <Area
dataKey='register' dataKey='register'
type='natural' type='natural'
@ -523,7 +601,7 @@ export default function Dashboard() {
</AreaChart> </AreaChart>
</ChartContainer> </ChartContainer>
</CardContent> </CardContent>
<CardFooter className='flex flex-row border-t p-4'> <CardFooter className='flex h-20 flex-row border-t p-4'>
<div className='flex w-full items-center gap-2'> <div className='flex w-full items-center gap-2'>
<div className='grid flex-1 auto-rows-min gap-0.5'> <div className='grid flex-1 auto-rows-min gap-0.5'>
<div className='text-muted-foreground text-xs'> <div className='text-muted-foreground text-xs'>
@ -533,60 +611,146 @@ export default function Dashboard() {
</div> </div>
</div> </div>
</CardFooter> </CardFooter>
</TabsContent>
</Card> </Card>
</div> </Tabs>
<div className='grid gap-4 lg:grid-cols-2 xl:grid-cols-3'> <Card>
<Card className='xl:col-span-2'> <CardHeader className='flex !flex-row items-center justify-between'>
<CardHeader> <CardTitle></CardTitle>
<CardTitle></CardTitle> <Tabs value={timeFrame} onValueChange={setTimeFrame}>
<TabsList>
<TabsTrigger value='today'></TabsTrigger>
<TabsTrigger value='yesterday'></TabsTrigger>
</TabsList>
</Tabs>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Table> <div className='mb-6 flex items-center justify-between'>
<TableHeader> <h4 className='font-semibold'>{dataType === 'nodes' ? '节点流量' : '用户流量'}</h4>
<TableRow> <Select onValueChange={setDataType} defaultValue='nodes'>
<TableHead></TableHead> <SelectTrigger className='w-28'>
<TableHead></TableHead> <SelectValue placeholder='选择类型' />
<TableHead className='text-right'></TableHead> </SelectTrigger>
</TableRow> <SelectContent>
</TableHeader> <SelectItem value='nodes'></SelectItem>
<TableBody> <SelectItem value='users'></SelectItem>
{new Array(10) </SelectContent>
.toString() </Select>
.split(',')
.map((item, index) => (
<TableRow key={index}>
<TableCell>
<Badge>Trojan</Badge>
</TableCell>
<TableCell>
<div className='font-medium'></div>
<div className='text-muted-foreground hidden text-sm md:inline'>
127.0.0.1:443
</div> </div>
</TableCell> <ChartContainer
<TableCell className='text-right'>1,000 GB</TableCell> config={{
</TableRow> traffic: {
))} label: '流量',
</TableBody> color: 'hsl(var(--primary))',
</Table> },
type: {
label: '类型',
color: 'hsl(var(--muted))',
},
email: {
label: '邮箱',
color: 'hsl(var(--muted))',
},
label: {
color: 'hsl(var(--foreground))',
},
}}
className='max-h-80'
>
<BarChart data={currentData} layout='vertical' height={400}>
<CartesianGrid strokeDasharray='3 3' />
<XAxis
type='number'
tickLine={false}
axisLine={false}
tickFormatter={(value) => formatBytes(unitConversion('gbToBytes', value) || 0)}
/>
<YAxis
type='category'
dataKey='name'
tickLine={false}
axisLine={false}
interval={0}
tickMargin={0}
width={15}
tickFormatter={(value, index) => String(index + 1)}
/>
<ChartTooltip
content={
<ChartTooltipContent
label={false}
labelFormatter={(label) =>
dataType === 'nodes' ? `节点: ${label}` : `用户: ${label}`
}
/>
}
/>
<Bar dataKey='traffic' fill='hsl(var(--primary))' radius={[0, 4, 4, 0]}>
<LabelList
dataKey='name'
position='insideLeft'
offset={8}
className='fill-[--color-label]'
fontSize={12}
/>
</Bar>
</BarChart>
</ChartContainer>
</CardContent> </CardContent>
</Card> </Card>
<Card> </div>
<div className='grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4'>
{[
{
title: '广告位1',
value: '广告内容1',
onClick: () => console.log('广告位1 clicked'),
},
{
title: '广告位2',
value: '广告内容2',
onClick: () => console.log('广告位2 clicked'),
},
{
title: '广告位3',
value: '广告内容3',
onClick: () => console.log('广告位3 clicked'),
},
{
title: '广告位4',
value: '广告内容4',
onClick: () => console.log('广告位4 clicked'),
},
{
title: '广告位5',
value: '广告内容5',
onClick: () => console.log('广告位5 clicked'),
},
{
title: '广告位6',
value: '广告内容6',
onClick: () => console.log('广告位6 clicked'),
},
{
title: '广告位7',
value: '广告内容7',
onClick: () => console.log('广告位7 clicked'),
},
{
title: '广告位8',
value: '广告内容8',
onClick: () => console.log('广告位8 clicked'),
},
].map((item, index) => (
<Card key={index} onClick={item.onClick} className='cursor-pointer'>
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle>{item.title}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className='grid gap-4'> <CardContent>
{new Array(15) <div className='text-xl font-bold tabular-nums leading-none'>{item.value}</div>
.toString()
.split(',')
.map((item, index) => (
<div className='flex items-center gap-4' key={index}>
<div className='text-sm font-medium leading-none'>olivia.martin@email.com</div>
<div className='ml-auto font-medium'>100 GB</div>
</div>
))}
</CardContent> </CardContent>
</Card> </Card>
))}
</div> </div>
</div> </div>
); );