mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-02-16 21:31:10 -05:00
✨ feat(billing): Update Billing
This commit is contained in:
parent
5b3f4b493f
commit
078fc9d756
@ -1,757 +1,11 @@
|
|||||||
'use client';
|
import Billing from '@/components/billing';
|
||||||
|
import Statistics from '@/components/dashboard/statistics';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { formatBytes, unitConversion } from '@repo/ui/utils';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@shadcn/ui/card';
|
|
||||||
import {
|
|
||||||
ChartConfig,
|
|
||||||
ChartContainer,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
} from '@shadcn/ui/chart';
|
|
||||||
import {
|
|
||||||
Area,
|
|
||||||
AreaChart,
|
|
||||||
Bar,
|
|
||||||
BarChart,
|
|
||||||
CartesianGrid,
|
|
||||||
Label,
|
|
||||||
LabelList,
|
|
||||||
Pie,
|
|
||||||
PieChart,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
} from '@shadcn/ui/lib/recharts';
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@shadcn/ui/select';
|
|
||||||
import { Separator } from '@shadcn/ui/separator';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@shadcn/ui/tabs';
|
|
||||||
import { useLocale } from 'next-intl';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const UserStatisticsConfig = {
|
|
||||||
register: {
|
|
||||||
label: '注册',
|
|
||||||
color: 'hsl(var(--chart-1))',
|
|
||||||
},
|
|
||||||
new_purchase: {
|
|
||||||
label: '新购',
|
|
||||||
color: 'hsl(var(--chart-2))',
|
|
||||||
},
|
|
||||||
repurchase: {
|
|
||||||
label: '复购',
|
|
||||||
color: 'hsl(var(--chart-3))',
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
const IncomeStatisticsConfig = {
|
|
||||||
new_purchase: {
|
|
||||||
label: '新购',
|
|
||||||
color: 'hsl(var(--chart-1))',
|
|
||||||
},
|
|
||||||
repurchase: {
|
|
||||||
label: '复购',
|
|
||||||
color: 'hsl(var(--chart-2))',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 [dataType, setDataType] = useState<string | 'nodes' | 'users'>('nodes');
|
|
||||||
const [timeFrame, setTimeFrame] = useState<string | 'today' | 'yesterday'>('today');
|
|
||||||
|
|
||||||
const currentData = trafficData[dataType][timeFrame];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-1 flex-col gap-3'>
|
<div className='flex flex-1 flex-col gap-3'>
|
||||||
<div className='grid grid-cols-2 gap-3 md:grid-cols-4 lg:grid-cols-8'>
|
<Statistics />
|
||||||
{[
|
<Billing type='dashboard' />
|
||||||
{
|
|
||||||
title: '在线IP数',
|
|
||||||
value: '666',
|
|
||||||
icon: 'uil:network-wired',
|
|
||||||
onClick: () => console.log('在线IP数 clicked'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '在线节点数',
|
|
||||||
value: '99',
|
|
||||||
icon: 'uil:server-network',
|
|
||||||
onClick: () => console.log('在线节点数 clicked'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '离线节点数',
|
|
||||||
value: '1',
|
|
||||||
icon: 'uil:server-network-alt',
|
|
||||||
onClick: () => console.log('离线节点数 clicked'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '待处理工单',
|
|
||||||
value: '1',
|
|
||||||
icon: 'uil:clipboard-notes',
|
|
||||||
onClick: () => console.log('待处理工单 clicked'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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>
|
|
||||||
<CardContent>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>{item.value}</div>
|
|
||||||
</CardContent>
|
|
||||||
</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>
|
|
||||||
<ChartLegend content={<ChartLegendContent />} />
|
|
||||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
|
||||||
<Pie
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
type: 'new_purchase',
|
|
||||||
value: 200,
|
|
||||||
fill: 'var(--color-new_purchase)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'repurchase',
|
|
||||||
value: 187,
|
|
||||||
fill: 'var(--color-repurchase)',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
dataKey='value'
|
|
||||||
nameKey='type'
|
|
||||||
innerRadius={50}
|
|
||||||
strokeWidth={5}
|
|
||||||
>
|
|
||||||
<Label
|
|
||||||
content={({ viewBox }) => {
|
|
||||||
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
|
|
||||||
return (
|
|
||||||
<text
|
|
||||||
x={viewBox.cx}
|
|
||||||
y={viewBox.cy}
|
|
||||||
textAnchor='middle'
|
|
||||||
dominantBaseline='middle'
|
|
||||||
>
|
|
||||||
<tspan
|
|
||||||
x={viewBox.cx}
|
|
||||||
y={viewBox.cy}
|
|
||||||
className='fill-foreground text-2xl font-bold'
|
|
||||||
>
|
|
||||||
6,666
|
|
||||||
</tspan>
|
|
||||||
</text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Pie>
|
|
||||||
</PieChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
|
||||||
<div className='flex w-full items-center gap-2'>
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>总收入</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>6,666</div>
|
|
||||||
</div>
|
|
||||||
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>
|
|
||||||
{IncomeStatisticsConfig.new_purchase.label}
|
|
||||||
</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>123</div>
|
|
||||||
</div>
|
|
||||||
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>
|
|
||||||
{IncomeStatisticsConfig.repurchase.label}
|
|
||||||
</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>456</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='month'>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer config={IncomeStatisticsConfig} className='max-h-80 w-full'>
|
|
||||||
<BarChart
|
|
||||||
accessibilityLayer
|
|
||||||
data={[
|
|
||||||
{ date: '2024-10-01', new_purchase: 98, repurchase: 125 },
|
|
||||||
{ date: '2024-10-02', new_purchase: 110, repurchase: 140 },
|
|
||||||
{ date: '2024-10-03', new_purchase: 100, repurchase: 130 },
|
|
||||||
{ date: '2024-10-04', new_purchase: 115, repurchase: 145 },
|
|
||||||
{ date: '2024-10-05', new_purchase: 108, repurchase: 138 },
|
|
||||||
{ date: '2024-10-06', new_purchase: 120, repurchase: 150 },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey='date'
|
|
||||||
tickLine={false}
|
|
||||||
tickMargin={10}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={(value) => {
|
|
||||||
return new Date(value).toLocaleDateString(locale, {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Bar dataKey='new_purchase' fill='var(--color-new_purchase)' radius={4} />
|
|
||||||
<Bar dataKey='repurchase' fill='var(--color-repurchase)' radius={4} />
|
|
||||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
|
||||||
<ChartLegend content={<ChartLegendContent />} />
|
|
||||||
</BarChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
|
||||||
<div className='flex w-full items-center gap-2'>
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>总收入</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>654,321</div>
|
|
||||||
</div>
|
|
||||||
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>
|
|
||||||
{IncomeStatisticsConfig.new_purchase.label}
|
|
||||||
</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>123</div>
|
|
||||||
</div>
|
|
||||||
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>
|
|
||||||
{IncomeStatisticsConfig.repurchase.label}
|
|
||||||
</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>456</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='total'>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer config={IncomeStatisticsConfig} className='max-h-80 w-full'>
|
|
||||||
<AreaChart
|
|
||||||
accessibilityLayer
|
|
||||||
data={[
|
|
||||||
{ date: '2024-07', new_purchase: 98, repurchase: 125 },
|
|
||||||
{ date: '2024-08', new_purchase: 110, repurchase: 140 },
|
|
||||||
{ date: '2024-09', new_purchase: 100, repurchase: 130 },
|
|
||||||
{ date: '2024-10', new_purchase: 115, repurchase: 145 },
|
|
||||||
{ date: '2024-11', new_purchase: 108, repurchase: 138 },
|
|
||||||
{ date: '2024-12', new_purchase: 120, repurchase: 150 },
|
|
||||||
]}
|
|
||||||
margin={{
|
|
||||||
left: 12,
|
|
||||||
right: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey='date'
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
tickMargin={8}
|
|
||||||
tickFormatter={(value) => {
|
|
||||||
return new Date(value).toLocaleDateString(locale, {
|
|
||||||
month: 'short',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
|
||||||
cursor={false}
|
|
||||||
content={<ChartTooltipContent indicator='dot' />}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
dataKey='new_purchase'
|
|
||||||
type='natural'
|
|
||||||
fill='var(--color-new_purchase)'
|
|
||||||
fillOpacity={0.4}
|
|
||||||
stroke='var(--color-new_purchase)'
|
|
||||||
stackId='a'
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
dataKey='repurchase'
|
|
||||||
type='natural'
|
|
||||||
fill='var(--color-repurchase)'
|
|
||||||
fillOpacity={0.4}
|
|
||||||
stroke='var(--color-repurchase)'
|
|
||||||
stackId='a'
|
|
||||||
/>
|
|
||||||
<ChartLegend content={<ChartLegendContent />} />
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
|
||||||
<div className='flex w-full items-center gap-2'>
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>总收入</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>987,654,321</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</TabsContent>
|
|
||||||
</Card>
|
|
||||||
</Tabs>
|
|
||||||
<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={UserStatisticsConfig} className='mx-auto max-h-80'>
|
|
||||||
<PieChart>
|
|
||||||
<ChartLegend content={<ChartLegendContent />} />
|
|
||||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
|
||||||
<Pie
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
type: 'register',
|
|
||||||
value: 275,
|
|
||||||
fill: 'var(--color-register)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'new_purchase',
|
|
||||||
value: 200,
|
|
||||||
fill: 'var(--color-new_purchase)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'repurchase',
|
|
||||||
value: 187,
|
|
||||||
fill: 'var(--color-repurchase)',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
dataKey='value'
|
|
||||||
nameKey='type'
|
|
||||||
innerRadius={50}
|
|
||||||
strokeWidth={5}
|
|
||||||
>
|
|
||||||
<Label
|
|
||||||
content={({ viewBox }) => {
|
|
||||||
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
|
|
||||||
return (
|
|
||||||
<text
|
|
||||||
x={viewBox.cx}
|
|
||||||
y={viewBox.cy}
|
|
||||||
textAnchor='middle'
|
|
||||||
dominantBaseline='middle'
|
|
||||||
>
|
|
||||||
<tspan
|
|
||||||
x={viewBox.cx}
|
|
||||||
y={viewBox.cy}
|
|
||||||
className='fill-foreground text-3xl font-bold'
|
|
||||||
>
|
|
||||||
275
|
|
||||||
</tspan>
|
|
||||||
</text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Pie>
|
|
||||||
</PieChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
|
||||||
<div className='flex w-full items-center gap-2'>
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>
|
|
||||||
{UserStatisticsConfig.register.label}
|
|
||||||
</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>789</div>
|
|
||||||
</div>
|
|
||||||
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>
|
|
||||||
{UserStatisticsConfig.new_purchase.label}
|
|
||||||
</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>123</div>
|
|
||||||
</div>
|
|
||||||
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>
|
|
||||||
{UserStatisticsConfig.repurchase.label}
|
|
||||||
</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>456</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='month'>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer config={UserStatisticsConfig} className='max-h-80 w-full'>
|
|
||||||
<BarChart
|
|
||||||
accessibilityLayer
|
|
||||||
data={[
|
|
||||||
{ date: '2024-10-01', register: 215, new_purchase: 98, repurchase: 125 },
|
|
||||||
{ date: '2024-10-02', register: 240, new_purchase: 110, repurchase: 140 },
|
|
||||||
{ date: '2024-10-03', register: 225, new_purchase: 100, repurchase: 130 },
|
|
||||||
{ date: '2024-10-04', register: 250, new_purchase: 115, repurchase: 145 },
|
|
||||||
{ date: '2024-10-05', register: 235, new_purchase: 108, repurchase: 138 },
|
|
||||||
{ date: '2024-10-06', register: 260, new_purchase: 120, repurchase: 150 },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey='date'
|
|
||||||
tickLine={false}
|
|
||||||
tickMargin={10}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={(value) => {
|
|
||||||
return new Date(value).toLocaleDateString(locale, {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Bar dataKey='register' fill='var(--color-register)' radius={4} />
|
|
||||||
<Bar dataKey='new_purchase' fill='var(--color-new_purchase)' radius={4} />
|
|
||||||
<Bar dataKey='repurchase' fill='var(--color-repurchase)' radius={4} />
|
|
||||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
|
||||||
<ChartLegend content={<ChartLegendContent />} />
|
|
||||||
</BarChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
|
||||||
<div className='flex w-full items-center gap-2'>
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>
|
|
||||||
{UserStatisticsConfig.register.label}
|
|
||||||
</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>789</div>
|
|
||||||
</div>
|
|
||||||
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>
|
|
||||||
{UserStatisticsConfig.new_purchase.label}
|
|
||||||
</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>123</div>
|
|
||||||
</div>
|
|
||||||
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>
|
|
||||||
{UserStatisticsConfig.repurchase.label}
|
|
||||||
</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>456</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='total'>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer config={UserStatisticsConfig} className='max-h-80 w-full'>
|
|
||||||
<AreaChart
|
|
||||||
accessibilityLayer
|
|
||||||
data={[
|
|
||||||
{ date: '2024-07', register: 215, new_purchase: 98, repurchase: 125 },
|
|
||||||
{ date: '2024-08', register: 240, new_purchase: 110, repurchase: 140 },
|
|
||||||
{ date: '2024-09', register: 225, new_purchase: 100, repurchase: 130 },
|
|
||||||
{ date: '2024-10', register: 250, new_purchase: 115, repurchase: 145 },
|
|
||||||
{ date: '2024-11', register: 235, new_purchase: 108, repurchase: 138 },
|
|
||||||
{ date: '2024-12', register: 260, new_purchase: 120, repurchase: 150 },
|
|
||||||
]}
|
|
||||||
margin={{
|
|
||||||
left: 12,
|
|
||||||
right: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey='date'
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
tickMargin={8}
|
|
||||||
tickFormatter={(value) => {
|
|
||||||
return new Date(value).toLocaleDateString(locale, {
|
|
||||||
month: 'short',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
|
||||||
cursor={false}
|
|
||||||
content={<ChartTooltipContent indicator='dot' />}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
dataKey='register'
|
|
||||||
type='natural'
|
|
||||||
fill='var(--color-register)'
|
|
||||||
fillOpacity={0.4}
|
|
||||||
stroke='var(--color-register)'
|
|
||||||
stackId='a'
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
dataKey='new_purchase'
|
|
||||||
type='natural'
|
|
||||||
fill='var(--color-new_purchase)'
|
|
||||||
fillOpacity={0.4}
|
|
||||||
stroke='var(--color-new_purchase)'
|
|
||||||
stackId='a'
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
dataKey='repurchase'
|
|
||||||
type='natural'
|
|
||||||
fill='var(--color-repurchase)'
|
|
||||||
fillOpacity={0.4}
|
|
||||||
stroke='var(--color-repurchase)'
|
|
||||||
stackId='a'
|
|
||||||
/>
|
|
||||||
<ChartLegend content={<ChartLegendContent />} />
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
|
||||||
<div className='flex w-full items-center gap-2'>
|
|
||||||
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
|
||||||
<div className='text-muted-foreground text-xs'>
|
|
||||||
{UserStatisticsConfig.register.label}
|
|
||||||
</div>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>987,654,321</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</TabsContent>
|
|
||||||
</Card>
|
|
||||||
</Tabs>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className='flex !flex-row items-center justify-between'>
|
|
||||||
<CardTitle>流量排行</CardTitle>
|
|
||||||
<Tabs value={timeFrame} onValueChange={setTimeFrame}>
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value='today'>今日</TabsTrigger>
|
|
||||||
<TabsTrigger value='yesterday'>昨日</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className='mb-6 flex items-center justify-between'>
|
|
||||||
<h4 className='font-semibold'>{dataType === 'nodes' ? '节点流量' : '用户流量'}</h4>
|
|
||||||
<Select onValueChange={setDataType} defaultValue='nodes'>
|
|
||||||
<SelectTrigger className='w-28'>
|
|
||||||
<SelectValue placeholder='选择类型' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value='nodes'>节点</SelectItem>
|
|
||||||
<SelectItem value='users'>用户</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<ChartContainer
|
|
||||||
config={{
|
|
||||||
traffic: {
|
|
||||||
label: '流量',
|
|
||||||
color: 'hsl(var(--primary))',
|
|
||||||
},
|
|
||||||
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>
|
|
||||||
</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>
|
|
||||||
<CardTitle>{item.title}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className='text-xl font-bold tabular-nums leading-none'>{item.value}</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,11 @@
|
|||||||
import { PinContainer } from '@shadcn/ui/3d-pin';
|
import Billing from '@/components/billing';
|
||||||
import { Label } from '@shadcn/ui/label';
|
|
||||||
import { Separator } from '@shadcn/ui/separator';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@shadcn/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@shadcn/ui/tabs';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@shadcn/ui/tooltip';
|
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
import Image from 'next/legacy/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import AlipayF2F from './alipayf2f';
|
import AlipayF2F from './alipayf2f';
|
||||||
import Epay from './epay';
|
import Epay from './epay';
|
||||||
import StripeAlipay from './stripe-alipay';
|
import StripeAlipay from './stripe-alipay';
|
||||||
import StripeWeChatPay from './stripe-wechat-pay';
|
import StripeWeChatPay from './stripe-wechat-pay';
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const response = await (await fetch('https://pay.ppanel.dev/')).json();
|
|
||||||
const t = await getTranslations('payment');
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tabs defaultValue='Epay'>
|
<Tabs defaultValue='Epay'>
|
||||||
@ -36,46 +28,9 @@ export default async function Page() {
|
|||||||
<AlipayF2F />
|
<AlipayF2F />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{response?.list?.length > 0 && (
|
<div className='flex flex-col gap-3'>
|
||||||
<TooltipProvider>
|
<Billing type='payment' />
|
||||||
<Separator />
|
</div>
|
||||||
<div className='ml-2 mt-4 flex flex-wrap items-center gap-4'>
|
|
||||||
<Label className='w-full'>{t('payForRecommendations')}</Label>
|
|
||||||
{response.list?.map((item) => {
|
|
||||||
return (
|
|
||||||
<Tooltip key={item.id}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Link href={item.href} className='flex flex-col flex-wrap items-center gap-2'>
|
|
||||||
<Image src={item.logo} width={40} height={40} className='rounded-full' />
|
|
||||||
{item.name}
|
|
||||||
</Link>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className='bg-transparent pb-10'>
|
|
||||||
<PinContainer title={item.name} href={item.href}>
|
|
||||||
<div className='flex h-[20rem] w-[20rem] basis-full flex-col p-4 tracking-tight sm:basis-1/2'>
|
|
||||||
<h3 className='text-foreground max-w-xs pb-2 text-base font-bold'>
|
|
||||||
{item.name}
|
|
||||||
</h3>
|
|
||||||
<div className='mb-4 !p-0 text-base font-normal'>
|
|
||||||
<span className='text-muted-foreground'>{item.description}</span>
|
|
||||||
</div>
|
|
||||||
<div className='mt-4 flex w-full flex-1 rounded-lg'>
|
|
||||||
<Image
|
|
||||||
src={item.image}
|
|
||||||
width={288}
|
|
||||||
height={200}
|
|
||||||
className='h-auto w-full'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PinContainer>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
60
apps/admin/components/billing.tsx
Normal file
60
apps/admin/components/billing.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@shadcn/ui/avatar';
|
||||||
|
import { Card, CardDescription, CardHeader, CardTitle } from '@shadcn/ui/card';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@shadcn/ui/tooltip';
|
||||||
|
import { getLocale } from 'next-intl/server';
|
||||||
|
import Image from 'next/legacy/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
const BASE_URL = 'https://cdn.jsdelivr.net/gh/perfect-panel/ppanel-assets/billing/index.json';
|
||||||
|
|
||||||
|
interface BillingProps {
|
||||||
|
type: 'dashboard' | 'payment';
|
||||||
|
}
|
||||||
|
export default async function Billing({ type }: BillingProps) {
|
||||||
|
const locale = await getLocale();
|
||||||
|
const response = await fetch(BASE_URL, { cache: 'no-store' });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data[type].length === 0) return null;
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<h1 className='text mt-2 font-bold'>
|
||||||
|
<span>{locale === 'en-US' ? 'Advertisement' : '广告合作'}</span>
|
||||||
|
<span className='text-muted-foreground ml-2 text-xs'>
|
||||||
|
{locale === 'en-US'
|
||||||
|
? 'Ad revenue helps PPanel continue to release updates'
|
||||||
|
: '广告收入有助于 PPanel 继续发布更新'}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<div className='grid grid-cols-1 gap-3 md:grid-cols-3 lg:grid-cols-5'>
|
||||||
|
{data[type].map((item, index) => (
|
||||||
|
<Tooltip key={index}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Link href={item.href} target='_blank'>
|
||||||
|
<Card className='cursor-pointer'>
|
||||||
|
<CardHeader className='flex flex-row items-center gap-2 p-3'>
|
||||||
|
<Avatar>
|
||||||
|
<AvatarImage src={item.logo} />
|
||||||
|
<AvatarFallback>{item.title}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<CardTitle>{item.title}</CardTitle>
|
||||||
|
<CardDescription className='line-clamp-3'>{item.description}</CardDescription>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className='bg-muted text-muted-foreground'>
|
||||||
|
<p className='mb-1'>{item.description}</p>
|
||||||
|
<Image src={item.poster} width={400} height={300} unoptimized objectFit='cover' />
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
703
apps/admin/components/dashboard/statistics.tsx
Normal file
703
apps/admin/components/dashboard/statistics.tsx
Normal file
@ -0,0 +1,703 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { formatBytes, unitConversion } from '@repo/ui/utils';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@shadcn/ui/card';
|
||||||
|
import {
|
||||||
|
ChartConfig,
|
||||||
|
ChartContainer,
|
||||||
|
ChartLegend,
|
||||||
|
ChartLegendContent,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
} from '@shadcn/ui/chart';
|
||||||
|
import {
|
||||||
|
Area,
|
||||||
|
AreaChart,
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Label,
|
||||||
|
LabelList,
|
||||||
|
Pie,
|
||||||
|
PieChart,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from '@shadcn/ui/lib/recharts';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@shadcn/ui/select';
|
||||||
|
import { Separator } from '@shadcn/ui/separator';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@shadcn/ui/tabs';
|
||||||
|
import { useLocale } from 'next-intl';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const UserStatisticsConfig = {
|
||||||
|
register: {
|
||||||
|
label: '注册',
|
||||||
|
color: 'hsl(var(--chart-1))',
|
||||||
|
},
|
||||||
|
new_purchase: {
|
||||||
|
label: '新购',
|
||||||
|
color: 'hsl(var(--chart-2))',
|
||||||
|
},
|
||||||
|
repurchase: {
|
||||||
|
label: '复购',
|
||||||
|
color: 'hsl(var(--chart-3))',
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
const IncomeStatisticsConfig = {
|
||||||
|
new_purchase: {
|
||||||
|
label: '新购',
|
||||||
|
color: 'hsl(var(--chart-1))',
|
||||||
|
},
|
||||||
|
repurchase: {
|
||||||
|
label: '复购',
|
||||||
|
color: 'hsl(var(--chart-2))',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 Statistics() {
|
||||||
|
const locale = useLocale();
|
||||||
|
|
||||||
|
const [dataType, setDataType] = useState<string | 'nodes' | 'users'>('nodes');
|
||||||
|
const [timeFrame, setTimeFrame] = useState<string | 'today' | 'yesterday'>('today');
|
||||||
|
|
||||||
|
const currentData = trafficData[dataType][timeFrame];
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='grid grid-cols-2 gap-3 md:grid-cols-4 lg:grid-cols-8'>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: '在线IP数',
|
||||||
|
value: '666',
|
||||||
|
icon: 'uil:network-wired',
|
||||||
|
onClick: () => console.log('在线IP数 clicked'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '在线节点数',
|
||||||
|
value: '99',
|
||||||
|
icon: 'uil:server-network',
|
||||||
|
onClick: () => console.log('在线节点数 clicked'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '离线节点数',
|
||||||
|
value: '1',
|
||||||
|
icon: 'uil:server-network-alt',
|
||||||
|
onClick: () => console.log('离线节点数 clicked'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '待处理工单',
|
||||||
|
value: '1',
|
||||||
|
icon: 'uil:clipboard-notes',
|
||||||
|
onClick: () => console.log('待处理工单 clicked'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
<CardContent>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>{item.value}</div>
|
||||||
|
</CardContent>
|
||||||
|
</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>
|
||||||
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
|
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||||
|
<Pie
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
type: 'new_purchase',
|
||||||
|
value: 200,
|
||||||
|
fill: 'var(--color-new_purchase)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'repurchase',
|
||||||
|
value: 187,
|
||||||
|
fill: 'var(--color-repurchase)',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
dataKey='value'
|
||||||
|
nameKey='type'
|
||||||
|
innerRadius={50}
|
||||||
|
strokeWidth={5}
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
content={({ viewBox }) => {
|
||||||
|
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
|
||||||
|
return (
|
||||||
|
<text
|
||||||
|
x={viewBox.cx}
|
||||||
|
y={viewBox.cy}
|
||||||
|
textAnchor='middle'
|
||||||
|
dominantBaseline='middle'
|
||||||
|
>
|
||||||
|
<tspan
|
||||||
|
x={viewBox.cx}
|
||||||
|
y={viewBox.cy}
|
||||||
|
className='fill-foreground text-2xl font-bold'
|
||||||
|
>
|
||||||
|
6,666
|
||||||
|
</tspan>
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Pie>
|
||||||
|
</PieChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
||||||
|
<div className='flex w-full items-center gap-2'>
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>总收入</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>6,666</div>
|
||||||
|
</div>
|
||||||
|
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>
|
||||||
|
{IncomeStatisticsConfig.new_purchase.label}
|
||||||
|
</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>123</div>
|
||||||
|
</div>
|
||||||
|
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>
|
||||||
|
{IncomeStatisticsConfig.repurchase.label}
|
||||||
|
</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>456</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value='month'>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer config={IncomeStatisticsConfig} className='max-h-80 w-full'>
|
||||||
|
<BarChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={[
|
||||||
|
{ date: '2024-10-01', new_purchase: 98, repurchase: 125 },
|
||||||
|
{ date: '2024-10-02', new_purchase: 110, repurchase: 140 },
|
||||||
|
{ date: '2024-10-03', new_purchase: 100, repurchase: 130 },
|
||||||
|
{ date: '2024-10-04', new_purchase: 115, repurchase: 145 },
|
||||||
|
{ date: '2024-10-05', new_purchase: 108, repurchase: 138 },
|
||||||
|
{ date: '2024-10-06', new_purchase: 120, repurchase: 150 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey='date'
|
||||||
|
tickLine={false}
|
||||||
|
tickMargin={10}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={(value) => {
|
||||||
|
return new Date(value).toLocaleDateString(locale, {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar dataKey='new_purchase' fill='var(--color-new_purchase)' radius={4} />
|
||||||
|
<Bar dataKey='repurchase' fill='var(--color-repurchase)' radius={4} />
|
||||||
|
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||||
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
|
</BarChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
||||||
|
<div className='flex w-full items-center gap-2'>
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>总收入</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>654,321</div>
|
||||||
|
</div>
|
||||||
|
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>
|
||||||
|
{IncomeStatisticsConfig.new_purchase.label}
|
||||||
|
</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>123</div>
|
||||||
|
</div>
|
||||||
|
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>
|
||||||
|
{IncomeStatisticsConfig.repurchase.label}
|
||||||
|
</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>456</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value='total'>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer config={IncomeStatisticsConfig} className='max-h-80 w-full'>
|
||||||
|
<AreaChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={[
|
||||||
|
{ date: '2024-07', new_purchase: 98, repurchase: 125 },
|
||||||
|
{ date: '2024-08', new_purchase: 110, repurchase: 140 },
|
||||||
|
{ date: '2024-09', new_purchase: 100, repurchase: 130 },
|
||||||
|
{ date: '2024-10', new_purchase: 115, repurchase: 145 },
|
||||||
|
{ date: '2024-11', new_purchase: 108, repurchase: 138 },
|
||||||
|
{ date: '2024-12', new_purchase: 120, repurchase: 150 },
|
||||||
|
]}
|
||||||
|
margin={{
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey='date'
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickMargin={8}
|
||||||
|
tickFormatter={(value) => {
|
||||||
|
return new Date(value).toLocaleDateString(locale, {
|
||||||
|
month: 'short',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
cursor={false}
|
||||||
|
content={<ChartTooltipContent indicator='dot' />}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey='new_purchase'
|
||||||
|
type='natural'
|
||||||
|
fill='var(--color-new_purchase)'
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stroke='var(--color-new_purchase)'
|
||||||
|
stackId='a'
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey='repurchase'
|
||||||
|
type='natural'
|
||||||
|
fill='var(--color-repurchase)'
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stroke='var(--color-repurchase)'
|
||||||
|
stackId='a'
|
||||||
|
/>
|
||||||
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
||||||
|
<div className='flex w-full items-center gap-2'>
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>总收入</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>987,654,321</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</TabsContent>
|
||||||
|
</Card>
|
||||||
|
</Tabs>
|
||||||
|
<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={UserStatisticsConfig} className='mx-auto max-h-80'>
|
||||||
|
<PieChart>
|
||||||
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
|
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||||
|
<Pie
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
type: 'register',
|
||||||
|
value: 275,
|
||||||
|
fill: 'var(--color-register)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'new_purchase',
|
||||||
|
value: 200,
|
||||||
|
fill: 'var(--color-new_purchase)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'repurchase',
|
||||||
|
value: 187,
|
||||||
|
fill: 'var(--color-repurchase)',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
dataKey='value'
|
||||||
|
nameKey='type'
|
||||||
|
innerRadius={50}
|
||||||
|
strokeWidth={5}
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
content={({ viewBox }) => {
|
||||||
|
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
|
||||||
|
return (
|
||||||
|
<text
|
||||||
|
x={viewBox.cx}
|
||||||
|
y={viewBox.cy}
|
||||||
|
textAnchor='middle'
|
||||||
|
dominantBaseline='middle'
|
||||||
|
>
|
||||||
|
<tspan
|
||||||
|
x={viewBox.cx}
|
||||||
|
y={viewBox.cy}
|
||||||
|
className='fill-foreground text-3xl font-bold'
|
||||||
|
>
|
||||||
|
275
|
||||||
|
</tspan>
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Pie>
|
||||||
|
</PieChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
||||||
|
<div className='flex w-full items-center gap-2'>
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>
|
||||||
|
{UserStatisticsConfig.register.label}
|
||||||
|
</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>789</div>
|
||||||
|
</div>
|
||||||
|
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>
|
||||||
|
{UserStatisticsConfig.new_purchase.label}
|
||||||
|
</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>123</div>
|
||||||
|
</div>
|
||||||
|
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>
|
||||||
|
{UserStatisticsConfig.repurchase.label}
|
||||||
|
</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>456</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value='month'>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer config={UserStatisticsConfig} className='max-h-80 w-full'>
|
||||||
|
<BarChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={[
|
||||||
|
{ date: '2024-10-01', register: 215, new_purchase: 98, repurchase: 125 },
|
||||||
|
{ date: '2024-10-02', register: 240, new_purchase: 110, repurchase: 140 },
|
||||||
|
{ date: '2024-10-03', register: 225, new_purchase: 100, repurchase: 130 },
|
||||||
|
{ date: '2024-10-04', register: 250, new_purchase: 115, repurchase: 145 },
|
||||||
|
{ date: '2024-10-05', register: 235, new_purchase: 108, repurchase: 138 },
|
||||||
|
{ date: '2024-10-06', register: 260, new_purchase: 120, repurchase: 150 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey='date'
|
||||||
|
tickLine={false}
|
||||||
|
tickMargin={10}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={(value) => {
|
||||||
|
return new Date(value).toLocaleDateString(locale, {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar dataKey='register' fill='var(--color-register)' radius={4} />
|
||||||
|
<Bar dataKey='new_purchase' fill='var(--color-new_purchase)' radius={4} />
|
||||||
|
<Bar dataKey='repurchase' fill='var(--color-repurchase)' radius={4} />
|
||||||
|
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||||
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
|
</BarChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
||||||
|
<div className='flex w-full items-center gap-2'>
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>
|
||||||
|
{UserStatisticsConfig.register.label}
|
||||||
|
</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>789</div>
|
||||||
|
</div>
|
||||||
|
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>
|
||||||
|
{UserStatisticsConfig.new_purchase.label}
|
||||||
|
</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>123</div>
|
||||||
|
</div>
|
||||||
|
<Separator orientation='vertical' className='mx-2 h-10 w-px' />
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>
|
||||||
|
{UserStatisticsConfig.repurchase.label}
|
||||||
|
</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>456</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value='total'>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer config={UserStatisticsConfig} className='max-h-80 w-full'>
|
||||||
|
<AreaChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={[
|
||||||
|
{ date: '2024-07', register: 215, new_purchase: 98, repurchase: 125 },
|
||||||
|
{ date: '2024-08', register: 240, new_purchase: 110, repurchase: 140 },
|
||||||
|
{ date: '2024-09', register: 225, new_purchase: 100, repurchase: 130 },
|
||||||
|
{ date: '2024-10', register: 250, new_purchase: 115, repurchase: 145 },
|
||||||
|
{ date: '2024-11', register: 235, new_purchase: 108, repurchase: 138 },
|
||||||
|
{ date: '2024-12', register: 260, new_purchase: 120, repurchase: 150 },
|
||||||
|
]}
|
||||||
|
margin={{
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey='date'
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickMargin={8}
|
||||||
|
tickFormatter={(value) => {
|
||||||
|
return new Date(value).toLocaleDateString(locale, {
|
||||||
|
month: 'short',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
cursor={false}
|
||||||
|
content={<ChartTooltipContent indicator='dot' />}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey='register'
|
||||||
|
type='natural'
|
||||||
|
fill='var(--color-register)'
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stroke='var(--color-register)'
|
||||||
|
stackId='a'
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey='new_purchase'
|
||||||
|
type='natural'
|
||||||
|
fill='var(--color-new_purchase)'
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stroke='var(--color-new_purchase)'
|
||||||
|
stackId='a'
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey='repurchase'
|
||||||
|
type='natural'
|
||||||
|
fill='var(--color-repurchase)'
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stroke='var(--color-repurchase)'
|
||||||
|
stackId='a'
|
||||||
|
/>
|
||||||
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
||||||
|
<div className='flex w-full items-center gap-2'>
|
||||||
|
<div className='grid flex-1 auto-rows-min gap-0.5'>
|
||||||
|
<div className='text-muted-foreground text-xs'>
|
||||||
|
{UserStatisticsConfig.register.label}
|
||||||
|
</div>
|
||||||
|
<div className='text-xl font-bold tabular-nums leading-none'>987,654,321</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</TabsContent>
|
||||||
|
</Card>
|
||||||
|
</Tabs>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className='flex !flex-row items-center justify-between'>
|
||||||
|
<CardTitle>流量排行</CardTitle>
|
||||||
|
<Tabs value={timeFrame} onValueChange={setTimeFrame}>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value='today'>今日</TabsTrigger>
|
||||||
|
<TabsTrigger value='yesterday'>昨日</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className='mb-6 flex items-center justify-between'>
|
||||||
|
<h4 className='font-semibold'>{dataType === 'nodes' ? '节点流量' : '用户流量'}</h4>
|
||||||
|
<Select onValueChange={setDataType} defaultValue='nodes'>
|
||||||
|
<SelectTrigger className='w-28'>
|
||||||
|
<SelectValue placeholder='选择类型' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value='nodes'>节点</SelectItem>
|
||||||
|
<SelectItem value='users'>用户</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<ChartContainer
|
||||||
|
config={{
|
||||||
|
traffic: {
|
||||||
|
label: '流量',
|
||||||
|
color: 'hsl(var(--primary))',
|
||||||
|
},
|
||||||
|
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>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user