完善概览页,实现公告查询展示,引入 recharts 展示取用数据
This commit is contained in:
BIN
src/app/admin/(dashboard)/_assets/action-bill.webp
Normal file
BIN
src/app/admin/(dashboard)/_assets/action-bill.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src/app/admin/(dashboard)/_assets/action-buy.webp
Normal file
BIN
src/app/admin/(dashboard)/_assets/action-buy.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src/app/admin/(dashboard)/_assets/action-logout.webp
Normal file
BIN
src/app/admin/(dashboard)/_assets/action-logout.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
35
src/app/admin/(dashboard)/_client/chart.tsx
Normal file
35
src/app/admin/(dashboard)/_client/chart.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
'use client'
|
||||
|
||||
import { ChartConfig, ChartContainer } from "@/components/ui/chart"
|
||||
import { Area, AreaChart, Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from "recharts"
|
||||
import { addDays, format } from "date-fns"
|
||||
|
||||
const data = Array(100).fill(0).map((_, i) => {
|
||||
let time = new Date()
|
||||
time = addDays(time, i)
|
||||
return {
|
||||
time: format(time, `MM/dd`),
|
||||
count: Math.floor(Math.random() * 100) + 1,
|
||||
}
|
||||
})
|
||||
|
||||
const config = {
|
||||
count: {
|
||||
label: `套餐使用量`,
|
||||
color: `var(--color-primary)`,
|
||||
}
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function DashboardChart() {
|
||||
return (
|
||||
<ChartContainer config={config} className={`w-full h-full`}>
|
||||
<AreaChart data={data} margin={{top: 0, right: 20, left: 0, bottom: 0}}>
|
||||
<CartesianGrid vertical={false}/>
|
||||
<XAxis dataKey={`time`} tickLine={false} />
|
||||
<YAxis tickLine={false}/>
|
||||
<Tooltip animationDuration={100}/>
|
||||
<Area dataKey={`count`} radius={20} className="fill-[var(--color-primary)]"/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,23 @@
|
||||
import Page from '@/components/page'
|
||||
import Image from 'next/image'
|
||||
import banner from './_assets/banner.webp'
|
||||
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'
|
||||
import soon from './_assets/coming-soon.svg'
|
||||
import {getProfile} from '@/actions/auth'
|
||||
import {format} from 'date-fns'
|
||||
import {CheckCircleIcon, CircleAlertIcon, LinkIcon} from 'lucide-react'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {CheckCircleIcon, CircleAlertIcon} from 'lucide-react'
|
||||
import {Button, buttonVariants} from '@/components/ui/button'
|
||||
import RechargeModal from '@/components/composites/recharge'
|
||||
import {merge} from '@/lib/utils'
|
||||
|
||||
import banner from './_assets/banner.webp'
|
||||
import soon from './_assets/coming-soon.svg'
|
||||
import actionBill from './_assets/action-bill.webp'
|
||||
import actionBuy from './_assets/action-buy.webp'
|
||||
import actionLogout from './_assets/action-logout.webp'
|
||||
import Link from 'next/link'
|
||||
import { listAnnouncements } from '@/actions/announcement'
|
||||
import DashboardChart from './_client/chart'
|
||||
|
||||
export type DashboardPageProps = {}
|
||||
|
||||
export default async function DashboardPage(props: DashboardPageProps) {
|
||||
@@ -29,84 +36,104 @@ export default async function DashboardPage(props: DashboardPageProps) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 短效 */}
|
||||
<Card className={`col-start-1 row-start-2 py-4`}>
|
||||
<CardHeader>
|
||||
<CardTitle>短效动态套餐</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className={`flex-auto flex flex-col gap-2`}>
|
||||
<div className={`flex-1 flex items-center justify-between`}>
|
||||
<h4>包时</h4>
|
||||
<p className={`flex flex-col items-end`}>
|
||||
<span className={`text-sm text-weak`}>当日可提取数量</span>
|
||||
<span className={`text-lg`}>todo</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`border-b`}></div>
|
||||
<div className={`flex-1 flex items-center justify-between`}>
|
||||
<h4>包量</h4>
|
||||
<p className={`flex flex-col items-end`}>
|
||||
<span className={`text-sm text-weak`}>剩余可提取数量</span>
|
||||
<span className={`text-lg`}>todo</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 长效 */}
|
||||
<Card className={`col-start-2 row-start-2`}>
|
||||
<CardHeader>
|
||||
<CardTitle>长效动态套餐</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className={`flex-auto flex flex-col gap-2 items-center justify-center`}>
|
||||
<Image alt={`coming soon`} src={soon}/>
|
||||
<p>敬请期待</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 固定 */}
|
||||
<Card className={`col-start-3 row-start-2 py-4`}>
|
||||
<CardHeader className={`px-4`}>
|
||||
<CardTitle>固定IP套餐</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className={`flex-auto flex flex-col gap-2 items-center justify-center`}>
|
||||
<Image alt={`coming soon`} src={soon}/>
|
||||
<p>敬请期待</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* 磁贴集 */}
|
||||
<Pins/>
|
||||
|
||||
{/* 图表 */}
|
||||
<section className={`col-start-1 row-start-3 col-span-3 row-span-2 bg-card p-4 rounded-lg`}>
|
||||
<Tabs defaultValue={`dynamic`}>
|
||||
<TabsList>
|
||||
<TabsTrigger value={`dynamic`} className={`data-[state=active]:text-primary`}>动态 IP 套餐</TabsTrigger>
|
||||
<TabsTrigger value={`static`} className={`data-[state=active]:text-primary`}>静态 IP 套餐</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value={`dynamic`}>
|
||||
dynamic
|
||||
</TabsContent>
|
||||
<TabsContent value={`static`}>
|
||||
static
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</section>
|
||||
<Charts/>
|
||||
|
||||
{/* 信息 */}
|
||||
<UserCenter/>
|
||||
|
||||
{/* 通知 */}
|
||||
<Card className={`col-start-4 row-start-3 row-span-2`}>
|
||||
<CardHeader>
|
||||
<CardTitle>通知</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
todo
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Announcements/>
|
||||
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
async function Pins() {
|
||||
return <>
|
||||
{/* 短效 */}
|
||||
<Card className={`col-start-1 row-start-2 py-4`}>
|
||||
<CardHeader>
|
||||
<CardTitle>短效动态套餐</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className={`flex-auto flex flex-col gap-2`}>
|
||||
<div className={`flex-1 flex items-center justify-between`}>
|
||||
<h4>包时</h4>
|
||||
<p className={`flex flex-col items-end`}>
|
||||
<span className={`text-sm text-weak`}>当日可提取数量</span>
|
||||
<span className={`text-lg`}>todo</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`border-b`}></div>
|
||||
<div className={`flex-1 flex items-center justify-between`}>
|
||||
<h4>包量</h4>
|
||||
<p className={`flex flex-col items-end`}>
|
||||
<span className={`text-sm text-weak`}>剩余可提取数量</span>
|
||||
<span className={`text-lg`}>todo</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 长效 */}
|
||||
<Card className={`col-start-2 row-start-2`}>
|
||||
<CardHeader>
|
||||
<CardTitle>长效动态套餐</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className={`flex-auto flex flex-col gap-2 items-center justify-center`}>
|
||||
<Image alt={`coming soon`} src={soon}/>
|
||||
<p>敬请期待</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 固定 */}
|
||||
<Card className={`col-start-3 row-start-2 py-4`}>
|
||||
<CardHeader className={`px-4`}>
|
||||
<CardTitle>固定IP套餐</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className={`flex-auto flex flex-col gap-2 items-center justify-center`}>
|
||||
<Image alt={`coming soon`} src={soon}/>
|
||||
<p>敬请期待</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
}
|
||||
|
||||
async function Charts() {
|
||||
|
||||
const data = [
|
||||
{time: `2023-10-01`, count: 100},
|
||||
{time: `2023-10-02`, count: 50},
|
||||
{time: `2023-10-03`, count: 80},
|
||||
{time: `2023-10-04`, count: 200},
|
||||
{time: `2023-10-05`, count: 150},
|
||||
]
|
||||
|
||||
return (
|
||||
<Card className={`col-start-1 row-start-3 col-span-3 row-span-2`}>
|
||||
<CardContent className={`overflow-hidden`}>
|
||||
<Tabs defaultValue={`dynamic`} className='h-full gap-4'>
|
||||
<TabsList>
|
||||
<TabsTrigger value={`dynamic`} className={`data-[state=active]:text-primary`}>动态 IP 套餐</TabsTrigger>
|
||||
<TabsTrigger value={`static`} className={`data-[state=active]:text-primary`}>静态 IP 套餐</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={`dynamic`} className={`overflow-hidden`}>
|
||||
<DashboardChart/>
|
||||
</TabsContent>
|
||||
<TabsContent value={`static`} className={`flex flex-col items-center justify-center gap-2`}>
|
||||
<Image alt={`coming soon`} src={soon}/>
|
||||
<p>敬请期待</p>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
async function UserCenter() {
|
||||
|
||||
const resp = await getProfile()
|
||||
@@ -160,21 +187,66 @@ async function UserCenter() {
|
||||
<div className={`flex flex-col gap-3`}>
|
||||
<h4 className={`text-sm text-weak`}>快捷入口</h4>
|
||||
<div className={`flex justify-around gap-2`}>
|
||||
<Button theme={`ghost`} className={`flex flex-col gap-2 h-auto p-2`}>
|
||||
<LinkIcon className={`size-8`}/>
|
||||
<span>功能入口</span>
|
||||
</Button>
|
||||
<Button theme={`ghost`} className={`flex flex-col gap-2 h-auto p-2`}>
|
||||
<LinkIcon className={`size-8`}/>
|
||||
<span>功能入口</span>
|
||||
</Button>
|
||||
<Button theme={`ghost`} className={`flex flex-col gap-2 h-auto p-2`}>
|
||||
<LinkIcon className={`size-8`}/>
|
||||
<span>功能入口</span>
|
||||
</Button>
|
||||
<Link href="/admin/bills" className={merge(buttonVariants({variant: `ghost`}), `flex flex-col gap-2 py-2 px-3 h-auto`)}>
|
||||
<Image alt={`bill icon`} src={actionBill} height={48}/>
|
||||
<span className={`text-sm text-weak`}>我的帐单</span>
|
||||
</Link>
|
||||
<Link href='/admin/purchase' className={merge(buttonVariants({variant: `ghost`}), `flex flex-col gap-2 py-2 px-3 h-auto`)}>
|
||||
<Image alt={`buy icon`} src={actionBuy} height={48}/>
|
||||
<span className={`text-sm text-weak`}>购买产品</span>
|
||||
</Link>
|
||||
<Link href='/admin/profile' className={merge(buttonVariants({variant: `ghost`}), `flex flex-col gap-2 py-2 px-3 h-auto`)}>
|
||||
<Image alt={`logout icon`} src={actionLogout} height={48}/>
|
||||
<span className={`text-sm text-weak`}>个人中心</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
async function Announcements() {
|
||||
|
||||
const resp = await listAnnouncements({
|
||||
page: 1,
|
||||
size: 5,
|
||||
})
|
||||
|
||||
if (!resp.success) {
|
||||
return (
|
||||
<div className={`col-start-4 row-start-3 row-span-2 flex justify-center items-center`}>
|
||||
获取公告数据失败
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const announcements = resp.data.list
|
||||
|
||||
return (
|
||||
<Card className={`col-start-4 row-start-3 row-span-2`}>
|
||||
<CardHeader>
|
||||
<CardTitle>公告</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className={`flex-auto p-0`}>
|
||||
{announcements.length === 0
|
||||
? (
|
||||
<div className={`flex flex-col items-center justify-center gap-2 h-full`}>
|
||||
<Image alt={`coming soon`} src={soon}/>
|
||||
<p>暂无公告</p>
|
||||
</div>
|
||||
)
|
||||
: announcements.map(item => (
|
||||
<div key={item.id} className={merge(
|
||||
`transition-colors duration-150 ease-in-out`,
|
||||
`flex flex-col gap-1 px-4 py-2`,
|
||||
`hover:bg-muted cursor-pointer`,
|
||||
)}>
|
||||
<h4>{item.title}</h4>
|
||||
<p className={`text-sm text-weak`}>{format(item.created_at, 'yyyy-MM-dd HH:mm')}</p>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user