.hand
This commit is contained in:
@@ -1,19 +0,0 @@
|
|||||||
'use server'
|
|
||||||
|
|
||||||
import {PageRecord} from '@/lib/api'
|
|
||||||
import {Announcement} from '@/lib/models'
|
|
||||||
import {callByUser} from './base'
|
|
||||||
|
|
||||||
export async function listAnnouncements(props: {
|
|
||||||
page: number
|
|
||||||
size: number
|
|
||||||
title?: string
|
|
||||||
type?: number
|
|
||||||
status?: number
|
|
||||||
create_after?: Date
|
|
||||||
create_before?: Date
|
|
||||||
update_after?: Date
|
|
||||||
update_before?: Date
|
|
||||||
}) {
|
|
||||||
return await callByUser<PageRecord<Announcement>>('/api/announcement/list', props)
|
|
||||||
}
|
|
||||||
43
src/actions/dashboard.ts
Normal file
43
src/actions/dashboard.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import {callByUser} from './base'
|
||||||
|
|
||||||
|
export async function listInitialization(props: {
|
||||||
|
page: number
|
||||||
|
size: number
|
||||||
|
title?: string
|
||||||
|
type?: number
|
||||||
|
status?: number
|
||||||
|
create_after?: Date
|
||||||
|
create_before?: Date
|
||||||
|
update_after?: Date
|
||||||
|
update_before?: Date
|
||||||
|
short_term_package?: number
|
||||||
|
}) {
|
||||||
|
return await callByUser<{
|
||||||
|
short_term: string
|
||||||
|
short_term_monthly: string
|
||||||
|
long_term: string
|
||||||
|
long_term_monthly: string
|
||||||
|
list: {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
created_at: Date
|
||||||
|
}[]
|
||||||
|
}>('/api/announcement/list', props)
|
||||||
|
}
|
||||||
|
|
||||||
|
type listAccountReq = {
|
||||||
|
resource_no: number
|
||||||
|
create_after: Date
|
||||||
|
create_before: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
type listAccountResp = {
|
||||||
|
time: Date
|
||||||
|
count: number
|
||||||
|
}[]
|
||||||
|
|
||||||
|
export async function listAccount(props: listAccountReq) {
|
||||||
|
return await callByUser<listAccountResp>('/api/account/list', props)
|
||||||
|
}
|
||||||
@@ -1,35 +1,33 @@
|
|||||||
'use client'
|
import {ChartConfig, ChartContainer} from '@/components/ui/chart'
|
||||||
|
import {Area, AreaChart, Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis} from 'recharts'
|
||||||
import { ChartConfig, ChartContainer } from "@/components/ui/chart"
|
import {addDays, format} from 'date-fns'
|
||||||
import { Area, AreaChart, Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from "recharts"
|
import {listAccount} from '@/actions/dashboard'
|
||||||
import { addDays, format } from "date-fns"
|
import {ExtraReq, ExtraResp} from '@/lib/api'
|
||||||
|
|
||||||
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 = {
|
const config = {
|
||||||
count: {
|
count: {
|
||||||
label: `套餐使用量`,
|
label: `套餐使用量`,
|
||||||
color: `var(--color-primary)`,
|
color: `var(--color-primary)`,
|
||||||
}
|
},
|
||||||
} satisfies ChartConfig
|
} satisfies ChartConfig
|
||||||
|
|
||||||
export default function DashboardChart() {
|
type DashboardChartProps = {
|
||||||
|
data: ExtraResp<typeof listAccount>
|
||||||
|
}
|
||||||
|
|
||||||
|
async function DashboardChart(props: DashboardChartProps) {
|
||||||
return (
|
return (
|
||||||
<ChartContainer config={config} className={`w-full h-full`}>
|
<ChartContainer config={config} className="w-full h-full">
|
||||||
<AreaChart data={data} margin={{top: 0, right: 20, left: 0, bottom: 0}}>
|
<AreaChart data={props.data} margin={{top: 0, right: 20, left: 0, bottom: 0}}>
|
||||||
<CartesianGrid vertical={false}/>
|
<CartesianGrid vertical={false}/>
|
||||||
<XAxis dataKey={`time`} tickLine={false} />
|
<XAxis dataKey="time" tickLine={false}/>
|
||||||
|
<XAxis dataKey="time" tickLine={false}/>
|
||||||
<YAxis tickLine={false}/>
|
<YAxis tickLine={false}/>
|
||||||
<Tooltip animationDuration={100}/>
|
<Tooltip animationDuration={100}/>
|
||||||
<Area dataKey={`count`} radius={20} className="fill-[var(--color-primary)]"/>
|
<Area dataKey="count" radius={20} className="fill-[var(--color-primary)]"/>
|
||||||
|
<Area dataKey="count" radius={20} className="fill-[var(--color-primary)]"/>
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
export default DashboardChart
|
||||||
|
|||||||
@@ -1,117 +1,151 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
|
import {Tabs, TabsList, TabsTrigger, TabsContent} from '@/components/ui/tabs'
|
||||||
import DashboardChart from "./chart"
|
import DashboardChart from './chart'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import soon from '../_assets/coming-soon.svg'
|
import soon from '../_assets/coming-soon.svg'
|
||||||
import DatePicker from "@/components/date-picker"
|
import DatePicker from '@/components/date-picker'
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
import {Card, CardHeader, CardTitle, CardContent} from '@/components/ui/card'
|
||||||
import { Form, FormField } from "@/components/ui/form"
|
import {Form, FormField} from '@/components/ui/form'
|
||||||
import { Input } from "@/components/ui/input"
|
import {Input} from '@/components/ui/input'
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import {zodResolver} from '@hookform/resolvers/zod'
|
||||||
import { useForm } from "react-hook-form"
|
import {useForm} from 'react-hook-form'
|
||||||
import zod from 'zod'
|
import zod from 'zod'
|
||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import mask from '../_assets/Mask group.webp'
|
import mask from '../_assets/Mask group.webp'
|
||||||
import {Button} from '@/components/ui/button'
|
import {Button} from '@/components/ui/button'
|
||||||
|
import {useStatus} from '@/lib/states'
|
||||||
|
import {useState} from 'react'
|
||||||
|
import {useSearchParams} from 'next/navigation'
|
||||||
|
import {log} from 'console'
|
||||||
|
import {listAccount} from '@/actions/dashboard'
|
||||||
|
import {ExtraReq, ExtraResp} from '@/lib/api'
|
||||||
|
import {toast} from 'sonner'
|
||||||
|
import {addDays, format} from 'date-fns'
|
||||||
export default function Charts() {
|
export default function Charts() {
|
||||||
|
const dateStr = '2025-03-05'
|
||||||
|
const dateStrA = '2024-03-05'
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const dateA = new Date(dateStrA)
|
||||||
|
// const [submittedData, setSubmittedData] = useState<ExtraReq<typeof listAccount>>()
|
||||||
|
const [submittedData, setSubmittedData] = useState<ExtraResp<typeof listAccount>>([
|
||||||
|
{time: new Date(), count: 80},
|
||||||
|
{time: date, count: 100},
|
||||||
|
{time: dateA, count: 50},
|
||||||
|
// {time: `2023-10-03`, count: 80},
|
||||||
|
// {time: `2023-10-04`, count: 200},
|
||||||
|
// {time: `2023-10-05`, count: 150},
|
||||||
|
])
|
||||||
const data = [
|
const data = [
|
||||||
{ time: `2023-10-01`, count: 100 },
|
{time: `2023-10-01`, count: 100},
|
||||||
{ time: `2023-10-02`, count: 50 },
|
{time: `2023-10-02`, count: 50},
|
||||||
{ time: `2023-10-03`, count: 80 },
|
{time: `2023-10-03`, count: 80},
|
||||||
{ time: `2023-10-04`, count: 200 },
|
{time: `2023-10-04`, count: 200},
|
||||||
{ time: `2023-10-05`, count: 150 },
|
{time: `2023-10-05`, count: 150},
|
||||||
]
|
]
|
||||||
const formSchema = zod.object({
|
|
||||||
name: zod.string(),
|
|
||||||
age: zod.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
const formSchema = zod.object({
|
||||||
|
resource_no: zod.number().min(11, '请输入正确的套餐编号').max(11, '请输入正确的套餐编号').optional(),
|
||||||
|
create_after: zod.date().optional(),
|
||||||
|
create_before: zod.date().optional(),
|
||||||
|
})
|
||||||
type FormValues = zod.infer<typeof formSchema>
|
type FormValues = zod.infer<typeof formSchema>
|
||||||
|
|
||||||
const form = useForm<FormValues>({
|
const form = useForm<FormValues>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: '',
|
|
||||||
age: '',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
const handler = form.handleSubmit(
|
||||||
|
async (value) => {
|
||||||
|
const res = {
|
||||||
|
resource_no: value.resource_no ?? 0,
|
||||||
|
create_after: value.create_after ?? new Date(),
|
||||||
|
create_before: value.create_before ?? new Date(),
|
||||||
|
}
|
||||||
|
const resp = await listAccount(res)
|
||||||
|
if (!resp.success) {
|
||||||
|
toast.error('接口请求失败:' + resp.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.data.map((_, i) => {
|
||||||
|
let time = new Date()
|
||||||
|
time = addDays(time, i)
|
||||||
|
return {
|
||||||
|
time: format(time, `MM/dd`),
|
||||||
|
count: Math.floor(Math.random() * 100) + 1,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setSubmittedData(resp.data)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={`col-start-1 row-start-3 col-span-3 row-span-2`}>
|
<Card className="col-start-1 row-start-3 col-span-3 row-span-2">
|
||||||
<CardContent className={`overflow-hidden`}>
|
<CardContent className="overflow-hidden">
|
||||||
<Tabs defaultValue={`dynamic`} className="h-full gap-4">
|
<Tabs defaultValue="dynamic" className="h-full gap-4">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value={`dynamic`} className={`data-[state=active]:text-primary`}>
|
<TabsTrigger value="dynamic" className="data-[state=active]:text-primary">
|
||||||
{/* <Image src={mask} alt={`Mask group`} width={35} height={35} priority /> */}
|
{/* <Image src={mask} alt={`Mask group`} width={35} height={35} priority /> */}
|
||||||
动态 IP 套餐</TabsTrigger>
|
动态 IP 套餐
|
||||||
<TabsTrigger value={`static`} className={`data-[state=active]:text-primary`}>静态 IP 套餐</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="static" className="data-[state=active]:text-primary">静态 IP 套餐</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<Form
|
<Form<FormValues>
|
||||||
// className="space-y-6"
|
className={merge(`grid grid-cols-3 gap-4 items-start`)}
|
||||||
|
handler={handler}
|
||||||
form={form}
|
form={form}
|
||||||
className={merge(
|
|
||||||
`grid grid-cols-3 gap-4 items-start`,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
name="username"
|
name="resource_no"
|
||||||
label={<span className={`w-full flex justify-end`}>套餐编号</span>}
|
label={<span className="w-full flex justify-end">套餐编号</span>}
|
||||||
className={`grid grid-cols-[70px_1fr] grid-rows-[auto_auto] `}
|
className={`grid grid-cols-[70px_1fr] grid-rows-[auto_auto] `}
|
||||||
classNames={{
|
classNames={{
|
||||||
message: `col-start-2`,
|
message: `col-start-2`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ field }) => (
|
{({field}) => (
|
||||||
<Input {...field} className={`w-52`}/>
|
<Input {...field} className="w-52"/>
|
||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
<div className={`flex items-center `} >
|
<div className={`flex items-center `} >
|
||||||
<FormField
|
<FormField
|
||||||
name={`create_after`}
|
name="create_after"
|
||||||
label={<span className={`w-full flex justify-end`}>时间范围筛选</span>}
|
label={<span className="w-full flex justify-end">时间范围筛选</span>}
|
||||||
className={`grid grid-cols-[100px_1fr] `}
|
className={`grid grid-cols-[100px_1fr] `}
|
||||||
classNames={{
|
classNames={{
|
||||||
message: `col-start-2`,
|
message: `col-start-2`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ field }) => (
|
{({field}) => (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
className={`w-36`}
|
className="w-36"
|
||||||
placeholder={`开始时间`}
|
placeholder="开始时间"
|
||||||
format={`yyyy-MM-dd`}
|
format="yyyy-MM-dd"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
<span className={`px-1`}>-</span>
|
<span className="px-1">-</span>
|
||||||
<FormField name={`create_before`}>
|
<FormField name="create_before">
|
||||||
{({ field }) => (
|
{({field}) => (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
className={`w-36`}
|
className="w-36"
|
||||||
placeholder={`结束时间`}
|
placeholder="结束时间"
|
||||||
format={`yyyy-MM-dd`}
|
format="yyyy-MM-dd"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
<Button className={'h-9 w-20'} type="submit">
|
<Button className="h-9 w-20" type="submit">
|
||||||
<span>查询</span>
|
<span>查询</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
<TabsContent value={`dynamic`} className={`overflow-hidden`}>
|
<TabsContent value="dynamic" className="overflow-hidden">
|
||||||
<DashboardChart />
|
{submittedData && <DashboardChart data={submittedData}/>}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value={`static`} className={`flex flex-col items-center justify-center gap-2`}>
|
<TabsContent value="static" className="flex flex-col items-center justify-center gap-2">
|
||||||
<Image alt={`coming soon`} src={soon} />
|
<Image alt="coming soon" src={soon}/>
|
||||||
<p>敬请期待</p>
|
<p>敬请期待</p>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
import soon from '../_assets/coming-soon.svg'
|
|
||||||
import mask from '../_assets/Mask group.webp'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function Pins() {
|
|
||||||
|
|
||||||
|
|
||||||
return <>
|
|
||||||
|
|
||||||
{/* 短效 */}
|
|
||||||
<Card className={`col-start-1 row-start-2 py-4`}>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle><Image src={mask} alt={`Mask group`} width={35} height={35} priority /> 短效动态套餐</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><Image src={mask} alt={`Mask group`} width={35} height={35} priority /> 长效动态套餐</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className={`flex-auto flex flex-col gap-2`}>
|
|
||||||
{/* <Image alt={`coming soon`} src={soon} />
|
|
||||||
<p>敬请期待</p> */}
|
|
||||||
<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-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>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
91
src/app/admin/(dashboard)/_client/userCenter.tsx
Normal file
91
src/app/admin/(dashboard)/_client/userCenter.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
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 Link from 'next/link'
|
||||||
|
import actionBill from '../_assets/action-bill.webp'
|
||||||
|
import actionBuy from '../_assets/action-buy.webp'
|
||||||
|
import actionLogout from '../_assets/action-logout.webp'
|
||||||
|
|
||||||
|
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({theme: `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({theme: `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({theme: `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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default UserCenter
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
|
import {ReactNode} from 'react'
|
||||||
import { ReactNode } from 'react'
|
import {Metadata} from 'next'
|
||||||
import { Metadata } from 'next'
|
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,170 +1,191 @@
|
|||||||
import Page from '@/components/page'
|
import Page from '@/components/page'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'
|
||||||
import { getProfile } from '@/actions/auth'
|
import {format} from 'date-fns'
|
||||||
import { format } from 'date-fns'
|
import {merge} from '@/lib/utils'
|
||||||
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 banner from './_assets/banner.webp'
|
||||||
import actionBill from './_assets/action-bill.webp'
|
import {listInitialization} from '@/actions/dashboard'
|
||||||
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 Charts from './_client/charts'
|
||||||
import Pins from './_client/pins'
|
import UserCenter from './_client/userCenter'
|
||||||
|
import soon from './_assets/coming-soon.svg'
|
||||||
|
import mask from './_assets/Mask group.webp'
|
||||||
|
|
||||||
export type DashboardPageProps = {}
|
export type DashboardPageProps = {}
|
||||||
|
|
||||||
export default function DashboardPage(props: DashboardPageProps) {
|
export default async function DashboardPage(props: DashboardPageProps) {
|
||||||
return (
|
const resp = await listInitialization({
|
||||||
<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,
|
page: 1,
|
||||||
size: 5,
|
size: 5,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
return (
|
return (
|
||||||
<div className={`col-start-4 row-start-3 row-span-2 flex justify-center items-center`}>
|
<div className="col-start-4 row-start-3 row-span-2 flex justify-center items-center">
|
||||||
获取公告数据失败
|
获取公告数据失败
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const announcements = resp.data.list
|
const initData = resp.data
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={`col-start-4 row-start-3 row-span-2`}>
|
<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>
|
||||||
|
|
||||||
|
{/* 磁贴集 */}
|
||||||
|
{initData && (
|
||||||
|
<Pins
|
||||||
|
short_term={String(initData.short_term)}
|
||||||
|
short_term_monthly={String(initData.short_term_monthly)}
|
||||||
|
long_term={String(initData.long_term)}
|
||||||
|
long_term_monthly={String(initData.long_term_monthly)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 图表 */}
|
||||||
|
<Charts/>
|
||||||
|
|
||||||
|
{/* 信息 */}
|
||||||
|
<UserCenter/>
|
||||||
|
|
||||||
|
{/* 通知 */}
|
||||||
|
{initData && (
|
||||||
|
<Announcements list={initData.list}/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type DashboardChartProps = {
|
||||||
|
short_term: string
|
||||||
|
short_term_monthly: string
|
||||||
|
long_term: string
|
||||||
|
long_term_monthly: string
|
||||||
|
}
|
||||||
|
function Pins(props: DashboardChartProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
{/* 短效 */}
|
||||||
|
<Card className="col-start-1 row-start-2 py-4">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className={`flex justify-between gap-2`}>
|
<CardTitle>
|
||||||
|
<Image src={mask} alt="Mask group" width={35} height={35} priority/>
|
||||||
|
{' '}
|
||||||
|
短效动态套餐
|
||||||
|
</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">{props.short_term ? props.short_term : '1'}</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">{props.short_term_monthly}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 长效 */}
|
||||||
|
<Card className="col-start-2 row-start-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
<Image src={mask} alt="Mask group" width={35} height={35} priority/>
|
||||||
|
{' '}
|
||||||
|
长效动态套餐
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex-auto flex flex-col gap-2">
|
||||||
|
{/* <Image alt={`coming soon`} src={soon} />
|
||||||
|
<p>敬请期待</p> */}
|
||||||
|
<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">{props.long_term}</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">{props.long_term_monthly}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
list: {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
created_at: Date
|
||||||
|
} []
|
||||||
|
}
|
||||||
|
function Announcements(props: Props) {
|
||||||
|
return (
|
||||||
|
<Card className="col-start-4 row-start-3 row-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex justify-between gap-2">
|
||||||
<CardTitle>公告</CardTitle>
|
<CardTitle>公告</CardTitle>
|
||||||
<span className={`text-sm text-weak`}>查看更多</span>
|
<span className="text-sm text-weak">查看更多</span>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className={`flex-auto p-0`}>
|
<CardContent className="flex-auto p-0">
|
||||||
{announcements.length === 0
|
{!props.list.length
|
||||||
? (
|
? (
|
||||||
<div className={`flex flex-col items-center justify-center gap-2 h-full`}>
|
<div className="flex flex-col items-center justify-center gap-2 h-full">
|
||||||
{/* <Image alt={`coming soon`} src={soon} />
|
<Image alt="coming soon" src={soon}/>
|
||||||
<p>暂无公告</p> */}
|
<p>暂无公告</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: announcements.map(item => (
|
: props.list.map(item => (
|
||||||
<div key={item.id} className={merge(
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className={merge(
|
||||||
`transition-colors duration-150 ease-in-out`,
|
`transition-colors duration-150 ease-in-out`,
|
||||||
`flex flex-col gap-1 px-4 py-2`,
|
`flex flex-col gap-1 px-4 py-2`,
|
||||||
`hover:bg-muted cursor-pointer`,
|
`hover:bg-muted cursor-pointer`,
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<h4>{item.title}</h4>
|
<h4>{item.title}</h4>
|
||||||
<p className={`text-sm text-weak`}>{format(item.created_at, 'yyyy-MM-dd HH:mm')}</p>
|
<p className="text-sm text-weak">{format(item.created_at, 'yyyy-MM-dd HH:mm')}</p>
|
||||||
</div>
|
</div>
|
||||||
))
|
))}
|
||||||
}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user