Files
admin/src/app/(root)/(dashboard)/page.tsx

440 lines
14 KiB
TypeScript
Raw Normal View History

2026-06-01 16:02:27 +08:00
"use client"
import { format, subDays } from "date-fns"
import {
Activity,
ArrowDownRight,
ArrowUpRight,
BarChart3,
Calendar,
DoorOpenIcon,
Server,
TrendingUp,
UserPlus,
Users,
} from "lucide-react"
import Link from "next/link"
2026-06-01 16:02:27 +08:00
import { useMemo, useState } from "react"
import {
SimpleBarChart,
SimpleHorizontalBarChart,
SimpleLineChart,
} from "@/components/charts"
import { Page } from "@/components/page"
type TimeRange = "today" | "7d" | "30d" | "90d" | "custom"
const timeRangeLabels: Record<TimeRange, string> = {
today: "今日",
"7d": "近7天",
"30d": "近30天",
"90d": "近90天",
custom: "自定义",
}
function mulberry32(seed: number) {
let t = seed
return () => {
t |= 0
t = (t + 0x6d2b79f5) | 0
let n = Math.imul(t ^ (t >>> 15), 1 | t)
n = (n + Math.imul(n ^ (n >>> 7), 61 | n)) ^ n
return ((n ^ (n >>> 14)) >>> 0) / 4294967296
}
}
export default function DashboardPage() {
const [timeRange, setTimeRange] = useState<TimeRange>("7d")
const [showCustomPicker, setShowCustomPicker] = useState(false)
const [startDate, setStartDate] = useState("")
const [endDate, setEndDate] = useState("")
const chartData = useMemo(() => {
const days = 14
const seed = 42
const rng = mulberry32(seed)
const proxyData = []
const tradeData = []
const customerData = []
const extractData = []
for (let i = days - 1; i >= 0; i--) {
const date = subDays(new Date(), i)
const dateStr = format(date, "MM-dd")
proxyData.push({
date: dateStr,
value: Math.floor(rng() * 50) + 80,
})
tradeData.push({
date: dateStr,
value: Math.floor(rng() * 3000) + 5000,
})
customerData.push({
date: dateStr,
value: Math.floor(rng() * 20) + 5,
})
extractData.push({
date: dateStr,
value: Math.floor(rng() * 800) + 1200,
})
}
const userData = [
{ name: "张三", activity: 287 },
{ name: "李四", activity: 245 },
{ name: "王五", activity: 198 },
{ name: "赵六", activity: 156 },
{ name: "孙七", activity: 132 },
{ name: "周八", activity: 98 },
{ name: "吴九", activity: 76 },
]
return { proxyData, tradeData, customerData, extractData, userData }
}, [])
// 处理自定义日期按钮点击
const handleCustomClick = () => {
setShowCustomPicker(true)
setTimeRange("custom")
// 清空之前的日期
setStartDate("")
setEndDate("")
}
// 处理日期确认
const handleDateConfirm = () => {
if (startDate && endDate) {
console.log("自定义日期范围:", startDate, "~", endDate)
setShowCustomPicker(false)
}
}
2025-12-29 10:41:23 +08:00
2026-06-01 16:02:27 +08:00
// 处理取消
const handleCancel = () => {
setShowCustomPicker(false)
setTimeRange("7d")
2026-06-01 16:02:27 +08:00
}
2025-12-29 10:41:23 +08:00
return (
2026-06-01 16:02:27 +08:00
<Page className="overflow-auto">
{/* 欢迎栏 */}
<div className="bg-card rounded-lg border p-2 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h1 className="text-lg font-bold">IP代理管理控制台</h1>
<p className="text-sm text-muted-foreground mt-1">: --</p>
</div>
<div className="flex gap-2 flex-wrap">
<Link
href="/cust"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md border text-sm text-muted-foreground hover:bg-accent transition-colors"
>
<Users className="h-4 w-4" />
</Link>
<Link
href="/trade"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md border text-sm text-muted-foreground hover:bg-accent transition-colors"
>
<Activity className="h-4 w-4" />
</Link>
<Link
href="/gateway"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md border text-sm text-muted-foreground hover:bg-accent transition-colors"
>
<DoorOpenIcon className="h-4 w-4" />
</Link>
2025-12-29 10:41:23 +08:00
</div>
</div>
2026-06-01 16:02:27 +08:00
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* 指标卡片 */}
<div className="lg:col-span-2 space-y-4">
<div className="space-y-3">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
<StatCard
title="待认领客户数量"
value="--"
change="--"
trend="up"
icon={Users}
iconColor="text-cyan-600"
iconBg="bg-cyan-50 border-cyan-100"
/>
<StatCard
title="活跃代理数"
value="--"
change="--"
trend="up"
icon={Server}
iconColor="text-blue-600"
iconBg="bg-blue-50 border-blue-100"
/>
<StatCard
title="今日交易额"
value="--"
change="--"
trend="up"
icon={TrendingUp}
iconColor="text-amber-600"
iconBg="bg-amber-50 border-amber-100"
/>
<StatCard
title="今日提取数量"
value="--"
change="--"
trend="up"
icon={BarChart3}
iconColor="text-rose-600"
iconBg="bg-rose-50 border-rose-100"
/>
<StatCard
title="今日新增客户"
value="--"
change="--"
trend="up"
icon={UserPlus}
iconColor="text-blue-600"
iconBg="bg-blue-50 border-blue-100"
/>
</div>
{/* 时间筛选栏 */}
<div className="flex flex-col sm:flex-row sm:items-center justify-end gap-3">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground"></span>
<div className="h-4 w-px bg-border" />
<div className="flex rounded-md border overflow-hidden">
{(Object.keys(timeRangeLabels) as TimeRange[]).map(key => (
<button
key={key}
onClick={() => {
if (key === "custom") {
handleCustomClick()
} else {
setTimeRange(key)
setShowCustomPicker(false)
}
}}
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
timeRange === key && !showCustomPicker
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-accent"
}`}
>
{timeRangeLabels[key]}
</button>
))}
</div>
</div>
{/* 点击自定义时显示 */}
{showCustomPicker && (
<div className="flex gap-2 items-center p-1 rounded-md">
<input
type="date"
2026-06-01 16:02:27 +08:00
className="px-2 py-1 border rounded text-sm bg-background"
value={startDate}
onChange={e => setStartDate(e.target.value)}
/>
2026-06-01 16:02:27 +08:00
<span className="text-muted-foreground">~</span>
<input
type="date"
2026-06-01 16:02:27 +08:00
className="px-2 py-1 border rounded text-sm bg-background"
value={endDate}
onChange={e => setEndDate(e.target.value)}
/>
2026-06-01 16:02:27 +08:00
<button
onClick={handleDateConfirm}
disabled={!startDate || !endDate}
className="px-3 py-1 bg-primary text-primary-foreground rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
<button
onClick={handleCancel}
className="px-3 py-1 border rounded text-sm hover:bg-accent"
>
</button>
</div>
)}
</div>
2025-12-29 10:41:23 +08:00
</div>
2026-06-01 16:02:27 +08:00
{/* 图表区域 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* 每日活跃代理 */}
<div className="bg-card rounded-lg border p-4">
<h3 className="font-semibold text-sm mb-4"></h3>
<SimpleLineChart
data={chartData.proxyData}
dataKey="value"
xAxisKey="date"
stroke="#3b82f6"
height={250}
/>
2025-12-29 10:41:23 +08:00
</div>
2026-06-01 16:02:27 +08:00
{/* 每日交易额 */}
<div className="bg-card rounded-lg border p-4">
<h3 className="font-semibold text-sm mb-4"></h3>
<SimpleBarChart
data={chartData.tradeData}
dataKey="value"
xAxisKey="date"
fill="#f59e0b"
height={250}
/>
2025-12-29 10:41:23 +08:00
</div>
2026-06-01 16:02:27 +08:00
{/* 每日新增客户 */}
<div className="bg-card rounded-lg border p-4">
<h3 className="font-semibold text-sm mb-4"></h3>
<SimpleLineChart
data={chartData.customerData}
dataKey="value"
xAxisKey="date"
stroke="#10b981"
height={250}
/>
2025-12-29 10:41:23 +08:00
</div>
2026-06-01 16:02:27 +08:00
{/* 每日提取量 */}
<div className="bg-card rounded-lg border p-4">
<h3 className="font-semibold text-sm mb-4"></h3>
<SimpleBarChart
data={chartData.extractData}
dataKey="value"
xAxisKey="date"
fill="#ef4444"
height={250}
/>
2025-12-29 10:41:23 +08:00
</div>
</div>
</div>
2026-06-01 16:02:27 +08:00
{/*套餐统计 */}
<div className="lg:col-span-1 space-y-4">
<div className="bg-card rounded-lg border">
<div className="p-5 border-b">
<h2 className="font-semibold"></h2>
2025-12-29 10:41:23 +08:00
</div>
2026-06-01 16:02:27 +08:00
<div className="p-5">
<div className="flex flex-col gap-4">
<ResourceItem
label="长效套餐"
active="--"
total="--"
color="bg-blue-500"
/>
<ResourceItem
label="短效套餐"
active="--"
total="--"
color="bg-emerald-500"
/>
<div className="pt-3 border-t flex justify-between text-sm">
<span className="text-muted-foreground">IP</span>
<span className="font-semibold">--</span>
</div>
2025-12-29 10:41:23 +08:00
</div>
</div>
</div>
2026-06-01 16:02:27 +08:00
{/* 最近活跃用户 */}
<div className="bg-card rounded-lg border">
<div className="p-5 border-b">
<h2 className="font-semibold"></h2>
2025-12-29 10:41:23 +08:00
</div>
2026-06-01 16:02:27 +08:00
<div className="p-4">
<SimpleHorizontalBarChart
data={chartData.userData}
dataKey="activity"
labelKey="name"
fill="#6366f1"
width={300}
2025-12-29 10:41:23 +08:00
/>
</div>
</div>
</div>
</div>
2026-06-01 16:02:27 +08:00
</Page>
2025-12-29 10:41:23 +08:00
)
}
2026-06-01 16:02:27 +08:00
function StatCard({
2025-12-29 10:41:23 +08:00
title,
value,
change,
2026-06-01 16:02:27 +08:00
trend,
icon: Icon,
iconColor,
iconBg,
2025-12-29 10:41:23 +08:00
}: {
title: string
value: string
change: string
2026-06-01 16:02:27 +08:00
trend: "up" | "down"
icon: React.ComponentType<{ className?: string }>
iconColor: string
iconBg: string
2025-12-29 10:41:23 +08:00
}) {
return (
2026-06-01 16:02:27 +08:00
<div className="bg-card rounded-lg border p-4">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<p className="text-xs text-muted-foreground truncate">{title}</p>
<p className="text-xl font-bold mt-1 truncate">{value}</p>
<div className="flex items-center gap-1 mt-1.5">
{trend === "up" ? (
<ArrowUpRight className="h-3 w-3 text-green-600 shrink-0" />
) : (
<ArrowDownRight className="h-3 w-3 text-red-600 shrink-0" />
)}
<span
2026-06-01 16:02:27 +08:00
className={`text-xs font-medium ${trend === "up" ? "text-green-600" : "text-red-600"}`}
>
2025-12-29 10:41:23 +08:00
{change}
</span>
2026-06-01 16:02:27 +08:00
<span className="text-xs text-muted-foreground"></span>
2025-12-29 10:41:23 +08:00
</div>
</div>
2026-06-01 16:02:27 +08:00
<div className={`p-2 rounded-md border shrink-0 ${iconBg}`}>
<Icon className={`h-5 w-5 ${iconColor}`} />
2025-12-29 10:41:23 +08:00
</div>
</div>
</div>
)
}
2026-06-01 16:02:27 +08:00
function ResourceItem({
label,
active,
total,
color,
2025-12-29 10:41:23 +08:00
}: {
2026-06-01 16:02:27 +08:00
label: string
active: string
total: string
color: string
2025-12-29 10:41:23 +08:00
}) {
return (
2026-06-01 16:02:27 +08:00
<div>
<div className="flex justify-between text-sm mb-1.5">
<span className="text-muted-foreground">{label}</span>
<span className="font-medium">
{active} / {total}
2025-12-29 10:41:23 +08:00
</span>
</div>
2026-06-01 16:02:27 +08:00
<div className="w-full h-2 bg-muted rounded-full overflow-hidden">
2025-12-29 10:41:23 +08:00
<div
2026-06-01 16:02:27 +08:00
className={`h-full ${color} rounded-full transition-all`}
style={{ width: "0%" }}
/>
2025-12-29 10:41:23 +08:00
</div>
</div>
)
}