440 lines
14 KiB
TypeScript
440 lines
14 KiB
TypeScript
"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"
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 处理取消
|
|
const handleCancel = () => {
|
|
setShowCustomPicker(false)
|
|
setTimeRange("7d")
|
|
}
|
|
|
|
return (
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
<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"
|
|
className="px-2 py-1 border rounded text-sm bg-background"
|
|
value={startDate}
|
|
onChange={e => setStartDate(e.target.value)}
|
|
/>
|
|
<span className="text-muted-foreground">~</span>
|
|
<input
|
|
type="date"
|
|
className="px-2 py-1 border rounded text-sm bg-background"
|
|
value={endDate}
|
|
onChange={e => setEndDate(e.target.value)}
|
|
/>
|
|
<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>
|
|
</div>
|
|
|
|
{/* 图表区域 */}
|
|
<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}
|
|
/>
|
|
</div>
|
|
|
|
{/* 每日交易额 */}
|
|
<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}
|
|
/>
|
|
</div>
|
|
|
|
{/* 每日新增客户 */}
|
|
<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}
|
|
/>
|
|
</div>
|
|
|
|
{/* 每日提取量 */}
|
|
<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}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/*套餐统计 */}
|
|
<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>
|
|
</div>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 最近活跃用户 */}
|
|
<div className="bg-card rounded-lg border">
|
|
<div className="p-5 border-b">
|
|
<h2 className="font-semibold">最近活跃用户</h2>
|
|
</div>
|
|
<div className="p-4">
|
|
<SimpleHorizontalBarChart
|
|
data={chartData.userData}
|
|
dataKey="activity"
|
|
labelKey="name"
|
|
fill="#6366f1"
|
|
width={300}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Page>
|
|
)
|
|
}
|
|
|
|
function StatCard({
|
|
title,
|
|
value,
|
|
change,
|
|
trend,
|
|
icon: Icon,
|
|
iconColor,
|
|
iconBg,
|
|
}: {
|
|
title: string
|
|
value: string
|
|
change: string
|
|
trend: "up" | "down"
|
|
icon: React.ComponentType<{ className?: string }>
|
|
iconColor: string
|
|
iconBg: string
|
|
}) {
|
|
return (
|
|
<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
|
|
className={`text-xs font-medium ${trend === "up" ? "text-green-600" : "text-red-600"}`}
|
|
>
|
|
{change}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">较昨日</span>
|
|
</div>
|
|
</div>
|
|
<div className={`p-2 rounded-md border shrink-0 ${iconBg}`}>
|
|
<Icon className={`h-5 w-5 ${iconColor}`} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ResourceItem({
|
|
label,
|
|
active,
|
|
total,
|
|
color,
|
|
}: {
|
|
label: string
|
|
active: string
|
|
total: string
|
|
color: string
|
|
}) {
|
|
return (
|
|
<div>
|
|
<div className="flex justify-between text-sm mb-1.5">
|
|
<span className="text-muted-foreground">{label}</span>
|
|
<span className="font-medium">
|
|
{active} / {total}
|
|
</span>
|
|
</div>
|
|
<div className="w-full h-2 bg-muted rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full ${color} rounded-full transition-all`}
|
|
style={{ width: "0%" }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|