Files
web/src/app/admin/(dashboard)/page.tsx
2025-06-05 17:10:44 +08:00

172 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Page from '@/components/page'
import Image from 'next/image'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { getProfile } from '@/actions/auth'
import { format } from 'date-fns'
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 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 Charts from './_client/charts'
import Pins from './_client/pins'
export type DashboardPageProps = {}
export default function DashboardPage(props: DashboardPageProps) {
return (
<Page className={merge(
`flex-auto grid`,
`grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_300px]`,
`grid-rows-[150px_200px_minmax(150px,1fr)_minmax(150px,1fr)]`,
)}>
{/* banner */}
<section className={`col-start-1 row-start-1 col-span-3 relative rounded-lg overflow-hidden`}>
<Image src={banner} alt={`banner image`} className={`w-full h-full inset-0 absolute object-cover`} />
<div className={`flex flex-col absolute inset-0 justify-center px-8 gap-1`}>
<h3 className={`text-2xl text-primary font-medium`}>IP资源</h3>
<p className={`text-primary font-medium`}>//IP代理</p>
</div>
</section>
{/* 磁贴集 */}
<Pins />
{/* 图表 */}
<Charts />
{/* 信息 */}
<UserCenter />
{/* 通知 */}
<Announcements />
</Page>
)
}
async function UserCenter() {
const resp = await getProfile()
if (!resp.success) {
return (
<div className={`col-start-4 row-start-1 row-span-2 flex justify-center items-center`}>
</div>
)
}
const profile = resp.data
return (
<Card className={`col-start-4 row-start-1 row-span-2`}>
<CardContent className={`flex-auto flex flex-col justify-between`}>
<div className={`flex flex-col gap-1`}>
<p>{profile.phone}</p>
<p className={`text-sm text-weak`}>{`最后登录:${format(profile.last_login, 'yyyy-MM-dd HH:mm')}`}</p>
</div>
<div className={merge(
`flex justify-between p-2 rounded-md`,
profile.id_token ? `bg-done-muted` : `bg-warn-muted`,
)}>
{profile.id_token
? <>
<div className={`flex gap-2 items-center`}>
<CheckCircleIcon size={20} className={`text-done`} />
<span></span>
</div>
<div className={`flex flex-col items-end`}>
<span className={`text-sm`}>{profile.name}</span>
<span className={`text-xs text-weak`}>{profile.id_no}</span>
</div>
</>
: <>
<span className={`flex gap-2 items-center`}>
<CircleAlertIcon className={`text-warn`} />
<span></span>
</span>
<Button className={`h-9`}></Button>
</>
}
</div>
<div className={`flex flex-col gap-1`}>
<h4 className={`text-sm text-weak`}></h4>
<div className={`flex justify-between items-baseline`}>
<p className={`text-xl text-accent`}>{profile.balance}</p>
<RechargeModal />
</div>
</div>
<div className={`flex flex-col gap-3`}>
<h4 className={`text-sm text-weak`}></h4>
<div className={`flex justify-around gap-2`}>
<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>
<div className={`flex justify-between gap-2`}>
<CardTitle></CardTitle>
<span className={`text-sm text-weak`}></span>
</div>
</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>
)
}